typescript - Menyimpulkan tipe spesifik dari sebuah union berdasarkan nilai diskriminator


Saya telah menggunakan tagged unions (juga dikenal sebagai "algebraic datatypes") dalam kode TypeScript saya, dan saya sangat senang dengan kode yang dihasilkan pola ini.

Namun, saya sering mengalami situasi seperti contoh berikut ini berulang kali:

// 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}`;
  }
})();

Dengan menggunakan pola ini, saya dapat memanfaatkan inferensi tipe TypeScript dan secara otomatis memeriksa keluasan tipe union PaymentMethod['method']. Kekurangannya adalah sintaks dari match ini yang berat dan sulit diatur.

Untuk tujuan ini, saya ingin membuat fungsi utilitas yang akan terlihat seperti ini:

// 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}`,
});

Semua ini bekerja, kecuali satu bagian - saya tidak bisa mengetahui bagaimana cara menyimpulkan tipe aktual dari varian union berdasarkan nilai 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 --*/;

Apakah mungkin untuk menyimpulkan tipe T dengan nilai tag tertentu? Saya telah mencari konstruk seperti T dimana T[K] adalah C, tetapi saya belum dapat menemukan konstruk seperti itu.


Edit: Berikut ini tautan Playground untuk menunjukkan contoh secara langsung.

Jawaban #1

Terima kasih kepada @spender untuk "saus ajaib"!

Persimpangan dari union yang ditandai dan nilai tag-nya sebenarnya memang menyimpulkan kunci yang tersisa untuk tag yang diberikan, sehingga itu dapat digunakan untuk mengimplementasikan fungsi pencocokan.

Berikut implementasi saya berdasarkan solusi spender, dengan tanda gelombang yang tersisa diatur:

// -- 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}`);

Saya juga menggunakan sintaks mirip Haskell untuk mendeklarasikan tipe tanda tangan fungsi sebelum fungsi, dan membuatnya sepenuhnya bertipe-curried untuk penggunaan fungsional maksimum.


Berikut adalah tautan Playground ke implementasinya, terima kasih atas bantuan Anda dan selamat bersenang-senang dengan fungsi utilitas ini.