- 文档
- 教程
【免费下载链接】typescript-book
The Concise TypeScript Book: A Concise Guide to Effective Development in TypeScript. Free and Open Source.
本文是开源项目《The Concise TypeScript Book》(关联文档)中"Unions discriminées(判别联合)"一章的深度讲解。文章以该章节的 Square/Circle/Shape 示例为主线,结合仓库中 union-type.md、narrowing.md、control-flow-analysis.md、exhaustiveness-checking.md 等相邻章节,系统说明判别联合的定义、判别属性的作用原理、与类型收窄的配合方式,以及如何借助never类型做穷尽性检查。读完本文,你将掌握用判别联合建模"多种形态"数据、在switch/if中安全收窄类型、并在新增分支时让编译器替你兜底的类型安全开发方案。
什么是判别联合
在 TypeScript 中,**判别联合(Discriminated Union,又称 Tagged Union / 可辨识联合)是一种特殊的联合类型:它通过一个公共属性(discriminant,判别属性)**来缩小联合中可能类型的集合。当联合的每个成员类型都拥有同一个属性,且该属性在各自成员中被声明为不同的字面量类型时,TypeScript 就能依据这个属性在运行时精确地区分每个成员。
其语法基础来自两类 TypeScript 核心机制:
- 联合类型(Union Type):表示一个值可以是若干类型之一,用
|符号连接每个可能的类型(参见 union-type.md):
let x: string | number; x = 'hello'; // Valid x = 123; // Valid- 字面量类型(Literal Type):某个集合中的单元素类型,精确对应一个 JavaScript 原始值。字符串、数字、布尔值都可以作为字面量类型(参见 literal-types.md):
const a = 'a'; // String literal type const b = 1; // Numeric literal type const c = true; // Boolean literal type type O = 'a' | 'b' | 'c'; // 只允许这三个字符串值判别联合正是这两者的结合:联合中每个成员都以不同的字面量值填充同一个判别属性,使该属性成为运行时判断的依据。
核心示例:用kind判别属性建模图形
原文档给出的经典示例是用一个kind属性区分正方形与圆形。kind在Square中被声明为字面量类型'square',在Circle中被声明为'circle',因此kind就是联合的判别属性(discriminant):
type Square = { kind: 'square'; // Discriminant size: number; }; type Circle = { kind: 'circle'; // Discriminant 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逐步拆解这个示例:
- 建模阶段:
Square拥有kind: 'square'与size: number;Circle拥有kind: 'circle'与radius: number。两个成员没有公共的"形状"字段,唯一共有的就是判别属性kind。 - 组合阶段:
type Shape = Square | Circle把两者合成为联合类型,一个Shape值要么是正方形、要么是圆形,不可能同时是两者。 - 消费阶段:
area函数以shape: Shape为参数,在switch (shape.kind)中对判别属性做等值比较。TS 编译器在case 'square'分支内将shape收窄为Square,因此可以安全访问shape.size;在case 'circle'分支内收窄为Circle,可以安全访问shape.radius。 - 结果验证:
area(square)输出25(即 5²),area(circle)输出12.566370614359172(即 π·2²),与运行时实际计算一致。
这里的核心要点是:判别属性必须使用字面量类型(而非string),因为字面量值才是编译期可识别的"标签"。若把kind声明为普通string,编译器将无法在分支中区分两个成员。
判别联合的底层机制:类型收窄(Narrowing)
判别联合之所以能安全访问各分支的专属属性,依赖的是 TypeScript 的类型收窄(narrowing)机制——在条件块内部,将变量的类型精化为其联合类型的子集。仓库 narrowing.md 列举了多种收窄途径,其中与判别联合直接相关的是等值收窄(Equality narrowing):
Equality narrowing in TypeScript works by checking whether a variable is equal to a specific value or not, to narrow its type accordingly. It is used in conjunction with
switchstatements and equality operators such as===,!==,==, and!=to narrow down types.
const checkStatus = (status: 'success' | 'error') => { switch (status) { case 'success': return true; case 'error': return null; } };area示例中的switch (shape.kind)正是等值收窄在判别联合上的应用:判别属性的字面量值('square'/'circle')与case分支的字面量标签逐一匹配,编译器据此把联合收缩到单一成员。除switch外,if (shape.kind === 'square')同样可以触发收窄。
为了对比,narrowing.md 还介绍了另外四种收窄方式,它们适用于不同场景:
| 收窄方式 | 判别依据 | 适用场景 |
|---|---|---|
typeof类型守卫 | 变量的内建 JS 类型 | string \| number这类基础类型联合 |
| Truthiness 收窄 | 变量是否为真值 | string \| null等可空值 |
| 等值收窄(Equality) | 变量是否等于某特定值 | switch+===/!==/==/!=,判别联合的核心手段 |
in运算符收窄 | 属性是否存在于对象类型中 | 成员间没有公共判别属性的对象联合 |
instanceof收窄 | 对象的构造函数 | 基于类的联合 |
in运算符收窄尤其值得注意:当联合成员没有公共判别属性时(例如Dog有breed、Cat有likesCream),可以用'breed' in pet来判断成员类型(见 narrowing.md):
type Dog = { name: string; breed: string; }; type Cat = { name: string; likesCream: boolean; }; const getAnimalType = (pet: Dog | Cat) => { if ('breed' in pet) { return 'dog'; } else { return 'cat'; } };相比之下,判别联合让成员共享同一个判别属性,代码意图更明确,也更便于编译器做穷尽性检查。
判别属性与控制流分析(TypeScript 4.4+)
判别联合的收窄还依赖 TypeScript 的控制流分析(Control Flow Analysis)。仓库 control-flow-analysis.md 指出:从 TypeScript 4.4 起,控制流分析不仅作用于if语句内部的代码,还能作用于条件表达式,以及通过const变量间接引用的判别属性访问:
const f2 = ( obj: { kind: 'foo'; foo: string } | { kind: 'bar'; bar: number } ) => { const isFoo = obj.kind === 'foo'; if (isFoo) { obj.foo; } else { obj.bar; } };这里把判别属性比较的结果存入const isFoo,在if (isFoo)分支内,编译器仍能推导出obj已被收窄为{ kind: 'foo'; foo: string },因此obj.foo可以安全访问。该文档同时强调了一个容易踩坑的限制:收窄只在const变量上生效,且被判别对象不能在函数体内被重新赋值。例如let isString = typeof x === 'string'后再收窄会报错;obj = obj这类函数体内的赋值也会中断收窄(详见 control-flow-analysis.md)。此外,条件表达式中最多分析五级间接引用。
穷尽性检查:用never让编译器兜底
判别联合的另一个杀手级特性是穷尽性检查(Exhaustiveness Checking)。仓库 exhaustiveness-checking.md 定义:穷尽性检查确保switch或if语句处理了判别联合的所有可能分支。其实现方式是借助never类型:
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 } };原理如下:
- 当
switch覆盖了Direction的所有成员('up'、'down')后,default分支中的direction已被收窄为没有任何值的类型,即never。 - 将
direction赋给类型为never的变量exhaustiveCheck,类型上是合法的(never可赋值给任何类型,反过来任何非never值都无法赋值给never)。 - 如果日后给
Direction新增一个成员(如'left')而忘记在switch中处理,default分支里direction的类型将变成'left',const exhaustiveCheck: never = direction就会产生编译错误,提醒你补齐分支。
把这一模式套用到本文的area示例上,可以增强其健壮性:
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); default: const exhaustiveCheck: never = shape; return exhaustiveCheck; } };将来若新增type Triangle = { kind: 'triangle'; base: number; height: number }并把它并入Shape,编译器会立刻在default分支报错,提示area尚未处理三角形——这就是判别联合带来的"改一处、编译器帮你查全"的维护体验。
综合实战:用判别联合建模网络请求状态
将上述概念整合起来,一个典型的判别联合实战场景是建模异步请求的多种状态(在switch中逐类处理,并用never兜底保证未来扩展安全):
type RequestState = | { status: 'idle' } | { status: 'loading' } | { status: 'success'; data: string[] } | { status: 'error'; message: string }; const render = (state: RequestState) => { switch (state.status) { case 'idle': return 'Waiting for user action'; case 'loading': return 'Loading...'; case 'success': return `Loaded ${state.data.length} items`; // 此处 state 已收窄为 success 成员 case 'error': return `Error: ${state.message}`; // 此处 state 已收窄为 error 成员 default: const exhaustiveCheck: never = state; return exhaustiveCheck; } };这里status就是判别属性(四个不同的字符串字面量),每个分支都能安全访问各自专属字段(data、message),且四个case全被覆盖后default中的state收敛为never。这种写法避免了大段if嵌套和运行时类型断言(as),让数据流在类型层面变得自解释。
使用判别联合的最佳实践
结合原文档示例与仓库相邻章节,总结以下实践建议:
- 判别属性必须是字面量类型:只有
'square'、'circle'、'success'这类字面量(字符串、数字或布尔值)才能让编译器在编译期区分成员;使用普通string会让收窄失效。 - 判别属性最好语义化命名:
kind、type、status、state都是常见命名,语义清晰即可;判别属性应放在对象成员中且各成员保持一致的键名与位置。 - 配合
switch或if做等值收窄:switch (shape.kind)、if (shape.kind === 'square')都是合法触发点;比较结果存入const变量后可间接引用(需 TypeScript 4.4+,且对象不可在函数体内重新赋值,见 control-flow-analysis.md)。 - 用
never做穷尽性兜底:在switch的default分支声明const exhaustiveCheck: never = value,新增联合成员而忘记处理时编译器会报错(见 exhaustiveness-checking.md)。 - 判别联合优于"裸联合 + 运行时判断":相比依赖
in运算符或instanceof在运行时摸索成员身份(narrowing.md),判别联合把身份信息显式固化在数据模型里,可读性、可维护性与穷尽性保障都更强。
以上代码片段均可直接复制到 TypeScript 4.4+ 项目中编译运行。本书在不同语言版本中均有对应章节,中文版可参见 zh-cn/book/discriminated-unions.md,章节编排见 table-of-contents.md。把判别联合作为建模"多形态数据"的默认选择,配合收窄与穷尽性检查,就能在开发阶段消灭一大类类型错误,这也是本指南将其作为全书重点章节的原因。
- 文档
- 教程
【免费下载链接】typescript-book
The Concise TypeScript Book: A Concise Guide to Effective Development in TypeScript. Free and Open Source.
相关推荐
The Concise TypeScript Book 精讲:判别联合(Discriminated Unions)的类型收窄实战
The Concise TypeScript Book 精讲:判别联合(Discriminated Unions)的类型收窄实战 本篇为开源仓库 The Con
文档教程TypeScript 可辨识联合(Discriminated Unions)权威指南:判别属性、穷尽性检查与 Redux 实战——基于 typescript-book 深入解析
TypeScript 可辨识联合(Discriminated Unions)权威指南:判别属性、穷尽性检查与 Redux 实战——基于 typescript b
教程The Concise TypeScript Book 精讲:可区分联合(Discriminated Unions)的判别式与类型收窄实战
The Concise TypeScript Book 精讲:可区分联合(Discriminated Unions)的判别式与类型收窄实战 可区分联合(Disc
文档教程
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考