TypeScript 可辨识联合与穷尽检查
解释如何用共同字面量字段建模有限业务状态,并使用 never 在新增成员时获得编译期提醒。
[!info] related notes
- 前置:TypeScript 联合类型、收窄与类型守卫
- 运行时边界:TypeScript 静态类型与运行时校验
- React 应用:useReducer
- 项目入口:BodySense MOC
TypeScript 可辨识联合与穷尽检查
一句话定义
可辨识联合让每个联合成员拥有同一个字面量判别字段,使编译器能根据该字段准确选择成员。
type TurnAction =
| { type: 'reset' }
| { type: 'hydrate'; state: TurnState }
| { type: 'answer'; interactionId: string };
心智模型:把有限状态机写进类型
不要把可辨识联合只记成一种语法技巧。更稳的理解是:
它把“系统允许出现哪些状态、每个状态必须携带什么数据”编码进类型,让不可能状态尽量无法表示。
例如与其写:
type RequestState<T> = {
loading: boolean;
data?: T;
error?: Error;
};
让 loading=true、data 和 error 同时存在这种矛盾组合成为可能,不如把状态拆成有限 variant:
type RequestState<T> =
| { status: 'idle' }
| { status: 'loading' }
| { status: 'success'; data: T }
| { status: 'error'; error: Error };
于是 status 既是业务状态,也是编译器的“导航标记”:判断 status === 'success' 后,编译器就知道 data 必须存在。
有限业务状态
↓
共同判别字段 status / type / kind
↓
控制流 narrowing
↓
每个分支只暴露该状态合法的数据
↓
never exhaustive check
↓
新增状态时,遗漏处理变成编译错误
为什么适合业务状态
相比一组可选字段,可辨识联合把有效组合写进类型:
type RequestState<T> =
| { status: 'idle' }
| { status: 'loading' }
| { status: 'success'; data: T }
| { status: 'error'; error: Error };
success 必然有 data,error 必然有 error,因此不需要在每个使用点猜测字段组合。
穷尽检查
function assertNever(value: never): never {
throw new Error(`Unhandled state: ${JSON.stringify(value)}`);
}
function label(state: RequestState<unknown>): string {
switch (state.status) {
case 'idle': return '等待';
case 'loading': return '加载中';
case 'success': return '完成';
case 'error': return state.error.message;
default: return assertNever(state);
}
}
新增联合成员后,未同步处理的 switch 会报错。它把“记得更新所有分支”从人工约定变成编译器检查。
never 真正在证明什么
当所有合法 variant 都已经被排除后,剩余值理论上应该是 never。因此如果新增:
| { status: 'paused'; reason: string }
而 switch 没有处理它,那么 default 中的值就不再是 never,编译器会直接暴露遗漏。
noImplicitReturns 与此不是一回事:它检查的是“函数是否存在没有返回值的执行路径”,而不是“一个联合的所有成员是否都被显式处理”。一个 default: return 'unknown' 可以满足返回路径完整,却仍然掩盖新增 variant。
运行时边界
可辨识联合只在 TypeScript 已经拥有可信值之后发挥作用。网络 JSON 即使字段看起来有 type,也不能因为:
const event = json as StreamEvent;
就自动成为可信联合。外部数据仍应先经过 runtime parser / Schema,再进入联合类型。见 TypeScript 静态类型与运行时校验。
面试 / 交流表述
可辨识联合本质上是在 TypeScript 里建模有限状态机。每个 variant 用共同的字面量字段做 discriminant,把该状态需要的数据和状态本身绑定起来,所以能避免一堆 optional 字段产生非法组合。消费端再配合 control-flow narrowing 和
never做 exhaustive check,这样以后新增状态时,漏掉的处理分支会直接暴露成编译错误。
设计边界
- 判别字段应稳定、唯一且来自业务协议,例如
type、status、kind。 - 不要用多个互相矛盾的 boolean 表达有限状态。
never穷尽检查适合“应该严格知道全部成员”的内部封闭边界;对需要 forward compatibility 的外部协议解析器,可能还需要显式 unknown-version / fallback 策略。- 外部 JSON 仍需先运行时校验,才能安全成为该联合。