TypeScript 可辨识联合类型是什么?有什么用?
简化版
可辨识联合类型是在联合类型的每个成员里放一个共同的字面量字段,例如 type 或 kind,TypeScript 可以根据这个字段自动收窄类型,适合描述状态机、接口结果和复杂组件 props。
详细版
示例:
type Result =
| { type: 'success'; data: string }
| { type: 'error'; message: string };
function handle(result: Result) {
if (result.type === 'success') {
result.data;
} else {
result.message;
}
}
共同字段 type 就是辨识字段。
它让类型判断从“猜属性是否存在”变成“按状态明确分支”。
完整版教学
一、它解决联合类型分支难判断的问题
普通联合类型里,每个成员可能有不同字段。
如果没有稳定标记,代码只能用 in、类型断言或复杂判断来收窄。
可辨识联合类型给每个成员加同一个字面量字段,让分支语义清楚。
type Shape =
| { kind: 'circle'; radius: number }
| { kind: 'square'; size: number };
这里 kind 是共同字段。
'circle' 和 'square' 是字面量值。
TypeScript 看到 shape.kind === 'circle' 后,就知道它是圆形分支。
二、switch 分支里会自动收窄
可辨识联合最常见写法是配合 switch。
每个 case 对应一种明确状态。
在对应分支里,TS 会自动识别可访问字段。
function area(shape: Shape) {
switch (shape.kind) {
case 'circle':
return Math.PI * shape.radius ** 2;
case 'square':
return shape.size * shape.size;
}
}
数字例子:圆半径为 2 时面积约 12.56,正方形边长为 2 时面积是 4。
不同分支需要不同字段,如果不收窄就无法安全访问。
三、它适合表达异步状态
前端异步请求经常有 loading、success、error 等状态。 如果用一堆可选字段描述,容易出现不可能状态。 可辨识联合能让每种状态只携带自己合法的数据。
type RequestState<T> =
| { status: 'idle' }
| { status: 'loading' }
| { status: 'success'; data: T }
| { status: 'error'; error: Error };
| 状态 | 合法字段 | 不应该出现的字段 |
|---|---|---|
loading | 无或进度 | data |
success | data | error |
error | error | data |
这能从类型层阻止“既有 data 又有 error”的混乱状态。
四、never 可以做穷尽性检查
当联合类型新增成员时,希望编译器提醒我们补充分支。
可以用 never 做穷尽性检查。
如果 switch 没覆盖所有情况,最后赋给 never 会报错。
function assertNever(x: never): never {
throw new Error(`Unexpected: ${x}`);
}
function render(state: RequestState<string>) {
switch (state.status) {
case 'idle':
case 'loading':
return 'loading';
case 'success':
return state.data;
case 'error':
return state.error.message;
default:
return assertNever(state);
}
}
流程如下:
联合类型成员
-> switch 覆盖每个辨识值
-> 剩余类型应为 never
-> 不为 never 说明漏分支
这对状态机代码很有价值。
五、组件 props 也常用这个模式
React 或 Vue 组件里,不同模式下 props 约束不同。 可辨识联合可以表达“传了 A 就必须传 B,不能传 C”。 这比一堆可选属性更安全。
type ButtonProps =
| { as: 'button'; onClick: () => void; href?: never }
| { as: 'a'; href: string; onClick?: never };
如果 as 是 'a',就必须传 href。
如果 as 是 'button',就必须传 onClick。
这种互斥关系用普通 interface 很难表达清楚。
记忆钩子:可辨识联合就是给每个分支挂一个门牌号,TS 按门牌号带你进入正确房间。
六、辨识字段要稳定且唯一
辨识字段通常叫 type、kind、status。
它的值应该是字面量类型,而不是宽泛的 string。
如果写成 type: string,TS 就无法根据具体值收窄到某个成员。
type Bad =
| { type: string; data: string }
| { type: string; error: Error };
这个写法失去了辨识能力。 更好的做法是给每个分支固定字面量值。
七、常见误区与追问
- 误区:联合类型天然就能安全访问所有字段。 只能访问所有成员共有字段,分支字段需要收窄后访问。
- 误区:辨识字段可以写成普通 string。 必须是可区分的字面量值,才能稳定收窄。
- 误区:可辨识联合只适合图形例子。 它非常适合接口结果、状态机、组件 props。
- 追问:如何检查 switch 是否漏分支? 用
never辅助函数做穷尽性检查。 - 追问:为什么比可选字段更好? 它能排除不可能状态,让每个分支只携带合法字段。
- 追问:辨识字段叫什么有要求吗? 名字没有硬性要求,常用
type/kind/status,关键是所有成员共享且值唯一。
八、加强记忆
可辨识联合按三步记:共同字段、字面量值、分支收窄。面试中用请求状态或组件 props 比图形例子更贴近前端工程,再补 never 穷尽性检查,答案就很有质感。