I've been using tagged unions (also known as "algebraic datatypes") in my TypeScript code, and I'm very happy with the code this pattern produces.
However, I've been running into situations like this following example repeatedly:
// 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}`;
}
})();
Using this pattern, I can leverage TypeScript's type inference and automatically check for exhaustiveness of the union type PaymentMethod['method']. The disadvantage is the unwieldy syntax of this match.
For this purpose, I wanted to build a utility function that would look something like this:
// 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}`,
});
This all works, except for one part - I can't figure out how to infer the actual type of the variant of the union based on the value of the 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 --*/;
Is it somehow possible to infer the type of T with a specific tag value? I have been looking for some construct like T where T[K] is C, but have not been able to find such a construct.
Edit: Here's a Playground Link to show the example live.