判别联合怎么配合 switch 做穷尽检查
How do discriminated unions enable exhaustiveness checking
一句话:判别联合(discriminated union, 也译可辨识联合)指每个成员都带同一个字段、 且字段类型是互不相同的字面量。switch 这个字段, 每个 case 里编译器自动收窄;default里把值赋给 never,将来加了成员忘了处理, 编译直接失败 —— 这就是穷尽检查(exhaustiveness check)。
穷尽检查的机制:控制流走到 default 时, 编译器算出「还没被 case 排除的成员」。三个成员都处理过, 剩下的是 never,const exhausted: never = s 成立; 第四个成员加进来却没写 case,剩下的就不是 never, 这一行立刻报错。错误出现在「忘了改的那处代码」, 而不是上线之后。
对比 as 断言:收窄的每一步都对应一个 真实的运行时检查,是「证明」;as不产生任何运行时代码,是「让编译器闭嘴」。JSON.parse(...) as Shape 能过编译, 但值长什么样由运行时说了算, 错误被推迟到离出错点很远的地方爆发。 as 有正当用途(类型守卫内部、测试代码、DOM 查询的细化), 但拿它替代收窄,等于手动关掉编译期的保障。
会追问:「typeof 收窄为什么不够用?」—— typeof 对一切对象都返回 "object", 分不开对象联合的成员;判别联合就是给对象联合准备的收窄手段。 本站 foundations 的 TS 课里 SettledResult 按status 收窄就是同一招,这一题只是往深处多走一层。 「判别字段为什么必须是字面量类型?」—— 两个成员的字段如果都是 string,编译器无从区分。 还会问 assertNever:把 default 里那两行抽成function assertNever(x: never): never, 每个 switch 复用。
In one line: a discriminated union is one where every member carries the same field, each typed as a different literal. switch on that field and the compiler narrows automatically in every case; in default, assign the value to never, and if a member is added later without a case, compilation fails on the spot — that is exhaustiveness checking.
How the check works: when control flow reaches default, the compiler computes which members the cases have not eliminated. With all three handled, what remains is never, so const exhausted: never = s holds; add a fourth member without a case and what remains is no longer never, so that line errors immediately. The failure shows up at the code you forgot to change — not in production.
Contrast with as: every narrowing step corresponds to a real runtime check — it is proof. as emits no runtime code at all — it just silences the compiler. JSON.parse(...) as Shape compiles, but the runtime decides what the value actually looks like, and the error detonates far from where it was planted. as has legitimate uses (inside type guards, in test code, refining DOM queries), but using it in place of narrowing means switching off the compile-time guarantee by hand.
Follow-up: “Why is typeof narrowing not enough?” — typeof answers "object" for every object, so it cannot tell union members apart; discriminated unions are the narrowing tool built for object unions. The foundations TS lesson on this site narrows SettledResult by status with the same move — this question just goes one level deeper. “Why must the discriminant be a literal type?” — if both members type the field as string, the compiler has nothing to tell them apart by. Expect assertNever too: extract those two default lines into function assertNever(x: never): never and reuse it in every switch.