TypeScript区分联合类型:限制属性与StrictUnion传参问题解决
TypeScript联合类型的两个问题解决
问题1:禁止联合类型实例包含其他分支的独有属性
原始代码
type Base = { base : string, count : number } type ExtendedA = Base & { special : string } type ExtendedB = Base & { hidden : number } function EXAMPLE (payload : ExtendedA | ExtendedB) { if ("special" in payload) { payload.special } else { payload.hidden } }
问题描述
当创建类型为Parameters<typeof EXAMPLE>[0]的对象并指定hidden属性(属于ExtendedB)时,TypeScript不会阻止同时传入special属性——因为结构类型系统中,只要对象满足联合类型某一分支的所有必填项,就会被视为合法。如何确保这类对象只能属于其中一个分支,不能同时包含其他分支的独有属性?
解决方法
使用StrictUnion类型改造联合类型,将其他分支的独有属性标记为never(可选,但传入即报错):
// 提取联合类型所有分支的键 type UnionKeys<T> = T extends T ? keyof T : never; // 为每个分支添加其他分支键的never约束 type StrictUnionHelper<T, TAll> = T extends any ? T & Partial<Record<Exclude<UnionKeys<TAll>, keyof T>, never>> : never; // 生成严格联合类型 type StrictUnion<T> = StrictUnionHelper<T, T> // 修改EXAMPLE的参数类型 function EXAMPLE (payload : StrictUnion<ExtendedA | ExtendedB>) { if ("special" in payload) { payload.special } else { payload.hidden } }
此时如果创建同时包含hidden和special的对象,TypeScript会直接报错,因为special在ExtendedB分支的StrictUnion类型中被标记为never,不允许赋值。
问题2:StrictUnion类型下的类型守卫缩小失效问题
问题场景
使用StrictUnion改造后,尝试将payload传入对应分支的函数时出现类型错误:
type UnionKeys<T> = T extends T ? keyof T : never; type StrictUnionHelper<T, TAll> = T extends any ? T & Partial<Record<Exclude<UnionKeys<TAll>, keyof T>, never>> : never; type StrictUnion<T> = StrictUnionHelper<T, T> function SPECIAL (payload : ExtendedA) {} function HIDDEN (payload : ExtendedB) {} function EXAMPLE (payload : StrictUnion<ExtendedA | ExtendedB>) { if ("special" in payload) { SPECIAL(payload) // 类型错误:payload不能赋值给ExtendedA } else { HIDDEN(payload) // 类型错误:payload不能赋值给ExtendedB } }
问题原因
StrictUnion生成的类型是(ExtendedA & { hidden?: never }) | (ExtendedB & { special?: never }),TypeScript的in类型守卫只能识别special属性存在,但无法自动将payload缩小到ExtendedA——因为交叉类型ExtendedA & { hidden?: never }和原始ExtendedA不是完全等价的类型。
解决方法
- 方法1:使用类型断言
在类型守卫后手动断言payload的类型:
function EXAMPLE (payload : StrictUnion<ExtendedA | ExtendedB>) { if ("special" in payload) { SPECIAL(payload as ExtendedA) } else { HIDDEN(payload as ExtendedB) } }
- 方法2:自定义类型谓词
创建更精确的类型守卫函数,明确判断payload属于哪个分支:
function isExtendedA(payload: StrictUnion<ExtendedA | ExtendedB>): payload is ExtendedA { return "special" in payload; } function isExtendedB(payload: StrictUnion<ExtendedA | ExtendedB>): payload is ExtendedB { return "hidden" in payload; } function EXAMPLE (payload : StrictUnion<ExtendedA | ExtendedB>) { if (isExtendedA(payload)) { SPECIAL(payload) // 类型自动缩小为ExtendedA } else { HIDDEN(payload) // 类型自动缩小为ExtendedB } }
- 方法3:调整StrictUnion的定义
让StrictUnion生成的类型可以被TypeScript自动识别为原始分支类型,比如去掉Partial(但这样会要求必须显式设置其他分支属性为undefined):
type StrictUnionHelper<T, TAll> = T extends any ? T & Record<Exclude<UnionKeys<TAll>, keyof T>, never> : never; type StrictUnion<T> = StrictUnionHelper<T, T> // 使用时必须显式设置其他分支属性为undefined const validB: StrictUnion<ExtendedA | ExtendedB> = { base: "test", count: 1, hidden: 2, special: undefined // 必须显式设置为undefined }
这种方法虽然能让类型守卫自动生效,但会增加使用成本,需要显式设置多余属性为undefined。
内容的提问来源于stack exchange,提问作者A. L
相关产品推荐
相关产品推荐

