TypeScript 可辨识联合(Discriminated Unions):用类型消灭状态错误
可辨识联合用共同的"标签字段"让 TypeScript 自动收窄类型。本文讲解其原理、状态机建模、穷尽性检查(never 技巧),以及 API 响应与事件系统的实战应用。
可辨识联合 = 多个对象类型共享一个字面量"标签字段"(如 type: 'success' | 'error')。TypeScript 能根据标签自动收窄类型:switch 到 case 'success' 分支后,编辑器精确知道当前是 SuccessResponse——写错字段名当场报红,整类运行期错误在编译期消失。
问题:松散联合的坑
没有辨识标签时,联合类型的字段访问处处受限:
// 反面教材
interface Result {
data?: User;
error?: string;
}
// data 和 error 都可能 undefined,用的时候全是防御性判断
?. 满天飞、if (result.data) 判不准——"同时可能有值"和"同时可能没值"的非法状态都合法,bug 由此滋生。
解法:标签字段 + 联合
interface Success { status: 'success'; data: User }
interface Failure { status: 'error'; message: string }
interface Pending { status: 'pending' }
type Result = Success | Failure | Pending;
function render(r: Result) {
switch (r.status) {
case 'success':
return r.data.name; // TS 知道这里是 Success
case 'error':
return r.message; // TS 知道这里是 Failure
case 'pending':
return '加载中…';
}
}
status 就是辨识标签(discriminant):字面量类型 + 共有字段,TS 的控制流分析自动完成收窄。
穷尽性检查:让编译器帮你兜住遗漏
新增一种状态时,怎么保证所有 switch 都被更新?用 never 兜底:
function assertNever(x: never): never {
throw new Error('未处理的分支: ' + JSON.stringify(x));
}
function render(r: Result) {
switch (r.status) {
case 'success': return r.data.name;
case 'error': return r.message;
default: return assertNever(r); // 漏了 pending 时编译报错
}
}
联合里新增成员后,所有没更新的 switch 全部编译报错——重构的安全性是类型系统白送的。
实战场景
- 状态机:
type State = Idle | Loading | Success | Failure,每个状态只携带自己需要的数据,消灭"loading 时 data 是什么"的糊涂账; - API 响应:
type Response = Ok<T> | Err,配合泛型interface Ok<T> { ok: true; value: T }; - 事件系统:
type Event = ClickEvent | ScrollEvent | KeyEvent,每种事件载荷不同,reducer 里 switch 精确收窄。
常见问题(FAQ)
Q:标签字段名有约定吗?
惯用 type、kind 或领域词(status)。整个项目统一一个名字最重要;跨库互操作时注意对齐(如 Redux 生态约定 type)。
Q:可辨识联合和枚举怎么分工?
枚举适合"一组固定值"(方向、级别);可辨识联合适合"值不同、携带的数据也不同"的变体。简单开关用枚举,结构化状态用联合——状态机几乎总是后者。
Q:嵌套的可辨识联合支持吗?
支持。外层收窄后内层继续收窄:if (r.status === 'success' && r.data.role === 'admin') 逐层精确。深层结构建议配合拆分小的类型守卫函数保持可读。
参考文档
本文依据公开文档整理,示例需按项目依赖版本验证;不将文档中的性能描述视为本项目实测结果。