TypeScript中如何根据判别式值推断联合类型的具体子类型
TypeScript 标记联合类型匹配工具的子类型提取方案
问题背景
在TypeScript项目中使用标记联合类型(tagged unions,又称代数数据类型)时,常规的switch是语句而非表达式,处理联合类型值时必须用立即执行函数包裹才能拿到返回值:
// 示例标记联合类型定义 type CheckNumber = string; type CreditCardType = 'visa' | 'mastercard'; type CreditCardNumber = string; type PaymentMethod = | { method: "cash" } | { method: "check", checkNumber: CheckNumber } | { method: "credit", cardType: CreditCardType, cardNumber: CreditCardNumber } const payment: PaymentMethod = { method: 'cash' }; // 必须包IIFE才能拿到switch的返回值 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}`; } })();
这种写法虽然可以借助TS的类型推断实现标签值的穷尽性检查,但语法冗余繁琐。
预期目标
实现一个通用匹配工具函数,用更简洁的表达式写法完成同样的类型安全匹配:
// 基于判别字段生成匹配函数 const matchPaymentMethod = matchingWith<PaymentMethod, 'method'>('method'); const payment: PaymentMethod = { method: 'credit', cardType: 'visa', cardNumber: '4111111111111111' }; // 表达式风格调用,每个分支参数自动推导为对应子类型 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 where T[K] = C的类型筛选能力。
实现方案
TypeScript内置的Extract工具类型完全可以满足这个筛选需求,不需要自定义复杂类型逻辑。
完整类型定义与实现
type MatchingWith = <T, K extends keyof T>(tagKey: K) => <R>( value: T, matching: { // 遍历联合类型所有可能的标签值 [C in T[K]]: (matched: Extract<T, Record<K, C>>) => R } ) => R; const matchingWith: MatchingWith = (tagKey) => (value, matching) => { const tag = value[tagKey]; return matching[tag as keyof typeof matching](value as never); }
关键逻辑说明
Extract<T, U>是TS内置的条件类型,作用是从联合类型T中筛选出可赋值给U的成员,正好对应T where T extends U的语义- 这里用
Record<K, C>构造“标签键K对应值为C”的对象结构,传给Extract就能精准筛选出对应标签的子类型 - 类型定义会自动遍历
T[K]的所有字面量类型作为匹配对象的键,自动实现穷尽性检查,漏写分支时TS会直接报类型错误 - 运行时逻辑仅做属性读取和函数调用,没有额外开销
类型效果验证
使用上述实现后,每个匹配分支的回调参数会自动推导为对应标签的子类型:
cash分支参数自动推导为{ method: 'cash' }check分支参数自动推导为{ method: 'check', checkNumber: CheckNumber }credit分支参数自动推导为{ method: 'credit', cardType: CreditCardType, cardNumber: CreditCardNumber }
完全保留原生switch的类型安全能力,同时写法更简洁,符合函数式表达式的使用习惯。
内容的提问来源于stack exchange,提问作者Alexander D.
相关产品推荐
相关产品推荐

