← [ BACK_TO_NOTES ]

TypeScript 可辨识联合与穷尽检查

解释如何用共同字面量字段建模有限业务状态,并使用 never 在新增成员时获得编译期提醒。

#concept #evergreen #dev #typescript

[!info] related notes

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 仍需先运行时校验,才能安全成为该联合。
创建于 2026/7/29 更新于 2026/9/10
Digital Biome
Access

键盘导航

j / k / ↑ / ↓
选择对象
Enter / o
打开选择
/ 或 Ctrl+K
全站搜索
i / f / 1–4
筛选当前列表
g → h / l / p / t / a
首页 / 笔记 / 项目 / 工具 / About
r → 1–9
打开所选对象的关联
u
返回上一页
Esc
关闭浮层 / 取消命令;输入框内清空文本