typescript - Suy diễn kiểu cụ thể của một hợp nhất dựa trên giá trị phân biệt
Tôi đã sử dụng các liên hợp có gán nhãn (còn được gọi là "kiểu dữ liệu đại số") trong mã TypeScript của mình, và tôi rất hài lòng với mã mà mẫu này tạo ra.
Tuy nhiên, tôi đã liên tục gặp phải những tình huống như ví dụ sau đây:
// 1. Define a tagged union (example taken from Scott Wlaschin, https://www.youtube.com/watch?v=2JB1_e5wZmU)
type PaymentMethod =
| { method: "cash" }
| { method: "check", checkNumber: CheckNumber }
| { method: "credit", cardType: CreditCardType, cardNumber: CreditCardNumber }
// 2. To evaluate a PaymentMethod value, I need to use an immediately-invoked function wrapping a switch-statement, since switch is not an expression
const payment: PaymentMethod = /*-- snip --*/;
const description = (() => {
switch (payment.method) {
case 'cash': return `Paid with cash`;
case 'check': return `Paid with check nr. ${payment.checkNumber}`;
case 'credit': return `Paid with a ${payment.cardType} credit card, nr. ${payment.cardNumber}`;
}
})();
Sử dụng mẫu này, tôi có thể tận dụng khả năng suy diễn kiểu của TypeScript và tự động kiểm tra tính đầy đủ của kiểu hợp PaymentMethod['method']. Nhược điểm là cú pháp của việc so khớp này khá cồng kềnh.
Vì mục đích này, tôi muốn xây dựng một hàm tiện ích sẽ trông như sau:
// 1. See declaration above
// 2.1. Instantiate a matching function using `PaymentMethod['method']` as discriminator
const matchPaymentMethod = matchingWith<PaymentMethod, 'method'>('method');
// 2.2. Use match function to evaluate PaymentMethod value
const payment: PaymentMethod = /*-- snip --*/;
const description = matchPaymentMethod(payment, {
cash: cash => `Paid with cash`,
check: check => `Paid with check nr. ${check.checkNumber}`,
credit: credit => `Paid with a ${credit.cardType} credit card, nr. ${credit.cardNumber}`,
});
Tất cả đều hoạt động, ngoại trừ một phần - tôi không thể figure được cách suy luận loại thực tế của biến thể của hợp nhất dựa trên giá trị của tag.
type MatchingWith<T, K extends keyof T> =
(tagKey: K)
=> <R>(value: T, matching: {[C in K]: (matched: /*??? Type of T with tag value C ???*/) => R})
=> R;
const matchingWith: MatchingWith<T> = /*-- snip --*/;
Có cách nào để suy ra loại của T dựa trên một giá trị nhãn cụ thể không? Tôi đã tìm kiếm một cấu trúc như T where T[K] is C, nhưng chưa thể tìm thấy cấu trúc như vậy.
Chỉnh sửa: Đây là một liên kết Playground để hiển thị ví dụ trực tiếp.
Câu trả lời #1
Cảm ơn @spender về "nước sốt ma thuật"!
Giao điểm của một liên kết được gắn thẻ và giá trị thẻ của nó thực sự suy ra các khóa còn lại cho thẻ nhất định, vì vậy có thể được sử dụng để triển khai hàm đối sánh.
Đây là cách triển khai của tôi dựa trên giải pháp của spender, với các dấu ngoặc sóng còn lại đã được sắp xếp:
// -- Declaration --
// 0. Define the type and implementation of the `matchingWith` function
type MatchingWith =
<T extends { [_ in K]: string | number | symbol }, K extends keyof T = keyof T>(tagKey: K) =>
<R, >(matching: { [M in T[K]]: (matched: T & { [_ in K]: M }) => R }) =>
(value: T) =>
R;
const matchingWith: MatchingWith = tagKey => matching => value => matching[value[tagKey]](value);
// -- Example --
// 1. Define a tagged union (example taken from Scott Wlaschin, https://www.youtube.com/watch?v=2JB1_e5wZmU)
type CheckNumber = string;
type CreditCardType = "ec" | "visa" | "mastercard";
type CreditCardNumber = string;
type PaymentMethod =
| { method: "cash" }
| { method: "check", checkNumber: CheckNumber }
| { method: "credit", cardType: CreditCardType, cardNumber: CreditCardNumber }
// 2. Instantiate a function creating PaymentMethod matches
const matchPaymentMethod = matchingWith<PaymentMethod>('method');
// 3. Instantiate a function to describe PaymentMethod values
const describePaymentMethod = matchPaymentMethod({
cash: cash => `Paid in cash`,
check: check => `Paid with check nr. ${check.checkNumber}`,
credit: credit => `Paid with ${credit.cardType} card nr. ${credit.cardNumber}`,
});
// 4. Create an example PaymentMethod value and evaluate it using `describePaymentMethod`
const exampleMethod: PaymentMethod = {method: 'credit', cardType: 'ec', cardNumber: '123456789'};
const exampleDescription = describePaymentMethod(exampleMethod);
console.log(`exampleDescription = ${exampleDescription}`);
Tôi cũng đã sử dụng cú pháp giống Haskell để khai báo kiểu chữ ký hàm trước hàm, và làm nó hoàn toàn curried để tối đa hóa việc sử dụng chức năng.
Đây là liên kết Playground đến phần triển khai, cảm ơn bạn đã giúp đỡ và chúc bạn vui với hàm tiện ích này.
