- 文档
- 教程
【免费下载链接】typescript-book
The Concise TypeScript Book: A Concise Guide to Effective Development in TypeScript. Free and Open Source.
判别联合(Discriminated Unions)是 TypeScript 类型系统中面向"数据形状多变"场景的核心建模工具:它以联合类型(Union Type)为基础,借助一个共享的字面量判别属性(Discriminant),让编译器在switch/if分支中自动收窄(narrow)变量类型,从而以类型安全的方式处理同一抽象下的不同变体。本文以《The Concise TypeScript Book》的 判别联合章节 为主线,结合本书仓库中关于类型收窄、控制流分析与穷尽性检查的相关章节,从定义、组成要素、完整示例到底层原理与工程实践逐层展开,读完你将能够用判别联合建模状态机、解析 AST、处理网络请求结果等典型场景,并配合never类型实现编译期的穷尽性校验。
一、什么是判别联合
在 Union Type 章节 中我们知道,联合类型用|符号表示"一个值可以是若干种类型之一":
let x: string | number; x = 'hello'; // Valid x = 123; // Valid但普通的联合类型在分支处理时很"笨重"——编译器只知道x可能是string或number,却不知道如何区分它们。判别联合正是在此基础上的一次升级:它是一种特殊的联合类型,利用每个成员共有的一个属性(称为判别属性 Discriminant,通常是字面量类型),把"可能是什么"收窄为"当前确定是什么"。
用书中原话概括:判别联合使用一个共同属性——判别属性(discriminant)——来缩小联合类型的可能类型集合。只要所有成员都具备该属性,且该属性在成员间取互不相同的字面量值,TypeScript 就能依据它的取值在分支中精确推断出当前成员的具体类型。
二、判别联合的三个组成要素
一个规范的判别联合由以下三部分构成:
| 要素 | 说明 | 示例 |
|---|---|---|
| 判别属性(Discriminant) | 每个成员类型共有的属性,通常命名为kind、type、status等,取值为字面量类型 | kind: 'square' |
| 字面量类型(Literal Types) | 判别属性必须是字符串、数字或布尔字面量类型,见 Literal Types 章节 | 'square'、'circle' |
| 联合类型(Union) | 将所有成员类型用|组合 | type Shape = Square \| Circle |
判别属性是关键。正如 Literal Types 章节 指出的,字面量类型是"集体类型内的单元素集合",字符串、数字、布尔字面量都可以作为判别值。它们被广泛用于联合、类型守卫(type guards)与类型别名中:
const a = 'a'; // String literal type const b = 1; // Numeric literal type const c = true; // Boolean literal type type O = 'a' | 'b' | 'c'; // 仅由指定值构成,其他字符串均不合法判别联合把这些字面量类型作为"标签"固定在每个成员上,使其成为可被编译器识别和区分的唯一标识。
三、完整示例:用判别联合计算图形面积
这是原文档给出的核心示例,它完整展示了判别联合的定义与使用流程。我们将它作为贯穿全文的基准案例:
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与Circle各自拥有一个kind字段,分别取字符串字面量'square'与'circle'——这就是判别属性;type Shape = Square | Circle把它们组合成判别联合;area函数接收Shape,在switch (shape.kind)中,TypeScript 依据case 'square'/case 'circle'将shape分别收窄为Square与Circle;- 收窄后,
shape.size与shape.radius都能被编译器正确识别,无需任何类型断言(type assertion)。
这里体现了判别联合相比"手动判断"的核心优势:分支内的属性访问是类型安全的。若在case 'circle'分支中错误地访问shape.size,编译器会立即报错,因为此时shape已经被收窄为Circle。
四、判别联合与类型收窄(Narrowing)
判别联合的威力必须通过类型收窄才能显现。在 Narrowing 章节 中,TypeScript 被定义为"在条件块内细化变量类型的过程",并列举了多种收窄手段。判别联合主要与其中两类配合使用:
4.1 Equality narrowing(等值收窄)
书中指出,等值收窄通过判断变量是否等于某个具体值来缩小类型,常与switch语句以及===、!==、==、!=运算符搭配使用:
const checkStatus = (status: 'success' | 'error') => { switch (status) { case 'success': return true; case 'error': return null; } };这正是判别联合的工作机制:case 'square'本质上是"判别属性等于字面量'square'"的等值判断,编译器由此把该分支中的shape收窄为Square。
4.2 In Operator narrowing(in运算符收窄)
当成员之间没有共有的判别属性时,可以改用in运算符判断属性是否存在来收窄:
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'; } };但需要注意的是,这种方式远不如判别联合可靠:它依赖"属性是否存在"而非"属性取值",当多个成员共享部分属性时容易产生歧义。相比之下,判别联合用唯一确定的字面量标签区分成员,语义更清晰、更不易出错。
此外,仓库的 Narrowing 章节 还介绍了typeof类型守卫(typeof x === 'number')、真值收窄(Truthiness narrowing,if (name))与instanceof收窄,这些适合处理原始类型、可空值与类实例,而判别联合则专攻"对象结构体"之间的区分。
五、判别联合 + never 类型 = 编译期穷尽性检查
判别联合最常见的进阶用法,是配合never类型实现穷尽性检查(Exhaustiveness checking)。仓库的 Exhaustiveness checking 章节 专门讲解了这个模式:
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 } };其原理正如该章节所述:never类型用于确保 default 分支是穷尽的,一旦 Direction 类型新增了值而没有在 switch 中被处理,TypeScript 就会在const exhaustiveCheck: never = direction处报错。因为此时direction被收窄为一个"不可能存在"的值,它只能赋给never;若联合中还有未被处理的成员,direction仍有实际类型,赋值就会失败。
关于never的语义,The never Type 章节 给出了补充说明:当变量被收窄到"不含任何值"的类型时,编译器会推断它为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}`); } };将两个章节的模式组合起来,就能得到工程上标准的"判别联合 + 穷尽性检查"样板:给判别联合的switch加上default分支,并把分支变量断言为never。未来任何开发者新增一个成员类型(例如为Shape增加Triangle)却忘记处理时,编译阶段就会立刻暴露问题,而不是等到运行时产生未定义行为。这相当于把"遗漏分支"这一运行时错误提前转移到了编译期。
六、底层原理:控制流分析如何驱动收窄
为什么判别联合能在分支中精确收窄类型?答案在 Control Flow Analysis 章节:TypeScript 会对代码流进行静态分析,推断变量在各代码路径上的类型,并依据分析结果自动收窄。
该章节还强调了一个版本相关的细节:TypeScript 4.4 之前,控制流分析只作用于if语句内部;从 4.4 起,它也能作用于条件表达式以及通过 const 变量间接引用的判别属性访问:
const f2 = ( obj: { kind: 'foo'; foo: string } | { kind: 'bar'; bar: number } ) => { const isFoo = obj.kind === 'foo'; if (isFoo) { obj.foo; } else { obj.bar; } };这里把obj.kind === 'foo'的结果存入 const 变量isFoo,再在if (isFoo)中访问obj.foo,编译器依然能完成收窄。该章节同时指出了收窄失效的两种情况:
const f1 = (x: unknown) => { let isString = typeof x === 'string'; // 注意:let 而非 const if (isString) { x.length; // Error,isString 不是 const,不触发收窄 } }; const f6 = ( obj: { kind: 'foo'; foo: string } | { kind: 'bar'; bar: number } ) => { const isFoo = obj.kind === 'foo'; obj = obj; // obj 在函数体内被重新赋值 if (isFoo) { obj.foo; // Error,obj 被赋值过,收窄失效 } };从中可以总结出编写判别联合代码的约束:判别条件应保存在 const 变量中,且被判断的对象在分支前不应被重新赋值;条件表达式中的间接引用分析最多支持五层间接(该章节明确注明了这一限制)。理解这些约束,能帮助你在实际编码时避免"明明写了判断却不收窄"的困惑。
七、工程实践:判别联合的建模选择与进阶技巧
7.1 用 type 而非 interface 定义判别联合
判别联合通常使用type关键字定义。仓库的 Differences between Type and Interface 章节 解释了原因:
- 接口支持声明合并:多个同名 interface 会被合并,这会破坏判别联合"成员可精确区分"的封闭性;
- 类型别名更灵活:
type可以用|直接组合联合类型,也可以组合接口:
interface A { x: 'x'; } interface B { y: 'y'; } type C = A | B; // 接口的联合虽然接口也能通过type C = A | B间接参与联合,但type声明联合与交叉类型的能力更直接、更内聚,因此判别联合的惯用写法是以type定义每个成员与整体联合。
7.2 用模板字面量类型扩展判别值
当判别属性需要表达组合语义时,可以结合 Template Union Types 章节 的模板字面量类型在类型系统中生成枚举值集合:
type Status = 'active' | 'inactive'; type Products = 'p1' | 'p2'; type ProductId = `id-${Products}-${Status}`; // "id-p1-active" | "id-p1-inactive" | "id-p2-active" | "id-p2-inactive"这类由模板字面量生成的联合值,同样可以作为判别属性的取值来源,让判别联合在更复杂的业务枚举下保持类型安全。
7.3 典型应用场景速览
判别联合在实际项目中高频出现的场景包括:
- 状态机建模:以
state: 'loading' | 'success' | 'error'为判别属性,每个状态携带不同的数据字段; - 网络请求结果:
Success(带data)与Failure(带error)的联合,避免"幽灵字段"; - AST 节点表示:每种节点类型共享
type判别属性,携带各自专有属性; - UI 组件变体:不同变体组件通过判别属性携带不同配置项,配合
switch渲染。
所有场景共享同一套模式:先定义带唯一字面量判别属性的成员类型,再组合成联合,最后在分支中依赖编译器自动收窄并配合never做穷尽性兜底。
八、结语
判别联合是 TypeScript 将"运行时分支逻辑"与"编译期类型安全"结合的典范。它的价值链条可以完整串起本书多个章节的知识:以 Literal Types 的字面量作为判别属性,以 Union Type 作为组合载体,借助 Narrowing 与控制流分析(Control Flow Analysis)在分支内收窄类型,再以 never 类型 实现 穷尽性检查。掌握这一模式,你的代码将获得两层保障:分支内绝无错误的属性访问,遗漏新成员时编译器第一时间报警。无论是重构既有代码还是设计新模块,判别联合都值得作为首选的数据建模方案。
本文基于开源仓库 typ/typescript-book(The Concise TypeScript Book)的俄语版 判别联合章节 展开,正文中的全部代码示例与原理说明均可在仓库 website/src/content/docs/book 目录下的对应章节文件中找到原文出处。
- 文档
- 教程
【免费下载链接】typescript-book
The Concise TypeScript Book: A Concise Guide to Effective Development in TypeScript. Free and Open Source.
相关推荐
TypeScript 判别联合(Discriminated Unions)实战指南:用判别属性实现精准类型收窄与穷尽性检查
TypeScript 判别联合(Discriminated Unions)实战指南:用判别属性实现精准类型收窄与穷尽性检查 本文是开源项目《The Concis
文档教程TypeScript 判别联合(Discriminated Unions)实战指南:用 kind 判别属性驱动类型收窄
TypeScript 判别联合(Discriminated Unions)实战指南:用 kind 判别属性驱动类型收窄 判别联合(Discriminated U
文档教程TypeScript 可辨识联合(Discriminated Unions)详解:从判别属性到穷尽性检查
TypeScript 可辨识联合(Discriminated Unions)详解:从判别属性到穷尽性检查 导读 可辨识联合(Discriminated Unio
文档教程
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考