:用 never 类型锁死未处理分支)
文档教程【免费下载链接】typescript-bookThe Concise TypeScript Book: A Concise Guide to Effective Development in TypeScript. Free and Open Source.项目地址https://gitcode.com/gh_mirrors/typ/typescript-book点击查看免费下载导读穷尽性检查Exhaustiveness Checking是 TypeScript 控制流分析Control Flow Analysis提供的一项能力当你在switch或if语句中处理一个可辨识联合Discriminated Union时编译器可以验证所有可能的成员是否都已被处理。本文以 The Concise TypeScript Book 俄语版《Проверка исчерпываемости》一节为骨架讲解如何利用never类型实现编译期穷尽性检查并深入结合本仓库英文原版及never、可辨识联合、窄化Narrowing等相邻章节的源码级内容给出可直接复制运行的完整示例。读完本文你将掌握在联合类型新增成员时、让编译器在构建期而非运行期暴露遗漏分支的实战方案。什么是穷尽性检查穷尽性检查是 TypeScript 内建的一项静态检查能力它确保一个可辨识联合discriminated union的所有可能情形都在switch语句或if语句中被逐一处理。其定义与完整示例位于英文原版文档 website/src/content/docs/book/exhaustiveness-checking.md 及俄语版 website/src/content/docs/ru-ru/book/exhaustiveness-checking.md 中原文档给出的基础示例type Direction up | down; const move (direction: Direction) { switch (direction) { case up: console.log(Moving up); break; case down: console.log(Moving down); break; default: const exhaustiveCheck: never direction; console.log(exhaustiveCheck); // This line will never be executed } };这段代码的要点在于default分支中的const exhaustiveCheck: never direction;当switch已经穷尽了Direction的全部两个取值up与down后default分支里的direction被编译器窄化narrow为不可能再出现的类型即never。把never类型的变量再赋值给一个声明为never的变量是合法的反之一旦Direction增加新成员而switch未同步处理direction在default中就会被推断为这个新类型例如left此时never left的赋值就会触发编译错误。never 类型穷尽性检查的基石要理解上面的代码必须先理解never类型。在 The Concise TypeScript Book 的 never-type.md 中定义never类型表示永远不会出现的值用于标注永远不会返回或必然抛出错误的函数或表达式。两个典型例子// 无限循环永不返回 const infiniteLoop (): never { while (true) { // do something } }; // 必然抛错永远走不到 return const throwError (message: string): never { throw new Error(message); };而与穷尽性检查关系最密切的是 the-never-type.md 中的描述当一个变量被窄化到不可能包含任何值的类型时TypeScript 编译器会推断该变量必然是never类型——因为never表示一个永远不会被产生的值。该文档中的示例const printValue (val: string | number) { if (typeof val string) { console.log(val.toUpperCase()); } else if (typeof val number) { console.log(val.toFixed(2)); } else { // val 在这里是 never 类型因为它不可能是 string 或 number 之外的任何值 const neverVal: never val; console.log(Unexpected value: ${neverVal}); } };这正是穷尽性检查在if/else链条上的形态所有已知分支处理完毕后else分支中的变量天然成为never。若未来为val的联合类型增加第三个成员编译器会在该else分支立刻报错从而强制开发者补全处理逻辑。在类型系统全景中exploring-the-type-system.md 用一个表格对never做了最直观的刻画never对应 ∅空集合其对立面是unknown全集。注意const x: never x这类赋值必然报错Type string is not assignable to type never——never是所有类型的子类型但反过来没有任何类型除它自身外可以赋给它。这一性质正是把剩余值塞进never变量来触发编译错误这一技巧成立的根本原因。结合可辨识联合为什么 switch 能自动穷尽穷尽性检查的使用场景几乎总是可辨识联合。在 discriminated-unions.md 中可辨识联合被定义为一种联合类型通过一个公共属性称为判别属性 discriminant来缩小联合的可能成员集合。典型例子type Square { kind: square; // 判别属性 size: number; }; type Circle { kind: circle; // 判别属性 radius: number; }; type Shape Square | Circle; const area (shape: Shape) { switch (shape.kind) { case square: return Math.pow(shape.size, 2); case circle: return Math.PI * Math.pow(shape.radius, 2); } }; const square: Square { kind: square, size: 5 }; const circle: Circle { kind: circle, radius: 2 }; console.log(area(square)); // 25 console.log(area(circle)); // 12.566370614359172在这个示例中switch (shape.kind)的每个case都会触发控制流分析control-flow-analysis.md进入case square后shape被窄化为Square于是可以安全访问shape.size同理case circle中可访问shape.radius。若后续有人给Shape增加type Rectangle { kind: rectangle; width: number; height: number }由于area没有default分支函数可能走完全部case后没有返回——此时配合strictNullChecks与noImplicitReturns编译器会提示并非所有代码路径都有返回值而如果加上穷尽性检查的default分支则会得到更精确的never类型不可赋值为Rectangle类错误提示信息直指新增成员。在 default 分支中落实穷尽性检查回到开头move函数把它升级为更贴近实战的写法——default分支不打印日志而是直接抛出错误该变体同样出现在 never-type.md 中type Direction up | down; const move (direction: Direction): void { switch (direction) { case up: // move up break; case down: // move down break; default: const exhaustiveCheck: never direction; throw new Error(Unhandled direction: ${exhaustiveCheck}); } };这段代码同时做了两件事运行期防御即便类型系统被绕过例如来自any或运行时数据未预期的direction值也会以明确错误信息抛出而不是静默地执行到函数末尾编译期契约const exhaustiveCheck: never direction;是哨兵赋值。switch穷尽全部成员后default里的direction已窄化为never赋值成立一旦Direction增加left而case left缺失direction在default中便是left类型never left直接编译失败错误精确指向这一行。用 if/else 实现同等的穷尽性穷尽性检查并非switch专属if语句同样适用。书中的the-never-type.md示例已展示了这一点const printValue (val: string | number) { if (typeof val string) { console.log(val.toUpperCase()); } else if (typeof val number) { console.log(val.toFixed(2)); } else { const neverVal: never val; throw new Error(Unexpected value: ${neverVal}); } };使用原则是先列出所有已知分支最后用一个else或default兜底并在兜底分支第一行放置never哨兵赋值。这样无论联合类型后续怎么扩展只要漏了分支构建阶段就会报错形成新增类型成员 → 编译失败 → 补全分支的强制闭环。这在领域模型使用字符串字面量联合、状态机、消息协议解析等场景中非常实用。为什么穷尽性检查值得成为代码规范从本仓库各语言版本如 zh-cn、ru-ru、es-es 等都能看到这本书把穷尽性检查作为独立章节反复强调其价值体现在三个方面把运行期错误提前到编译期分支遗漏是运行时 BUG 的高发源never哨兵让编译器替你把守配合窄化与判别属性实现类型安全可辨识联合 穷尽性检查是 TypeScript 中最常见的编译期状态机组合详见 narrowing.md 中关于 equality narrowing 的switch用法自我文档化default分支中的throw new Error(Unhandled ...)本身就是对该函数接受且只接受这些值的显式声明后续维护者一目了然。需要注意的是穷尽性检查依赖控制流分析对联合类型的完整推理。从 control-flow-analysis.md 可以看到两个边界条件其一窄化所依赖的中间变量必须是const若写成let isString typeof x stringif (isString)内不会发生窄化其二若在函数体内对判别对象重新赋值如obj obj编译器会放弃基于旧值的窄化。因此编写穷尽性检查代码时请保持哨兵判断基于const变量、并避免在判断之后对相关对象重新赋值。小结穷尽性检查是 TypeScript 用类型系统约束运行时行为的经典技巧核心只有三步定义可辨识联合或字面量联合 → 用switch/if穷举所有分支 → 在default/else中放入const exhaustiveCheck: never value;哨兵。只要联合类型将来新增成员而处理代码未同步更新构建就会立即失败把隐患消灭在编译期。相关源码与全量示例可在本仓库 website/src/content/docs/book/exhaustiveness-checking.md、website/src/content/docs/book/never-type.md 与 website/src/content/docs/book/the-never-type.md 中继续研读。赞分享文档教程【免费下载链接】typescript-bookThe Concise TypeScript Book: A Concise Guide to Effective Development in TypeScript. Free and Open Source.项目地址https://gitcode.com/gh_mirrors/typ/typescript-book点击查看免费下载相关推荐The Concise TypeScript Book 精读穷尽性检查Exhaustiveness Checking——用 never 类型捕获所有未处理分支The Concise TypeScript Book 精读穷尽性检查Exhaustiveness Checking——用 never 类型捕获所有未处理文档教程The Concise TypeScript Book 中的穷尽性检查Exhaustiveness Checking用 never 类型锁定所有联合类型分支The Concise TypeScript Book 中的穷尽性检查Exhaustiveness Checking用 never 类型锁定所有联合类型分文档教程The Concise TypeScript Book 精讲穷尽性检查Exhaustiveness Checking与 never 类型的实战用法The Concise TypeScript Book 精讲穷尽性检查Exhaustiveness Checking与 never 类型的实战用法 穷尽性文档教程上一篇Cycle.js函数式编程范式纯函数、不可变性与引用透明性实践下一篇MonoGame 3D游戏开发模型加载与动画系统完全指南创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考