- 文档
- 教程
【免费下载链接】typescript-book
The Concise TypeScript Book: A Concise Guide to Effective Development in TypeScript. Free and Open Source.
never是 TypeScript 类型系统中最容易被误解的类型之一:它既不等于undefined,也不等于void,而是代表"永远不可能产生的值"。本文以开源项目 The Concise TypeScript Book 中 法语版 the-never-type.md(及其对应的 英文版)为核心骨架,结合仓库中 narrowing.md、exhaustiveness-checking.md、control-flow-analysis.md 等章节,系统讲解never的语义、推断时机,以及它在穷尽性检查、防御式编程中的实战价值。读完本文,你将能准确识别"什么时候编译器会把类型收窄为never",并写出能在新增联合成员时立即报错的健壮代码。
一、never是什么:代表"不可能产生的值"的类型
按照本书的定义:
当一个变量被收窄(narrowed)到某个不可能包含任何值的类型时,TypeScript 编译器会推断该变量必然是
never类型。这是因为never类型代表一个永远不可能被产生的值。
这段话包含两个关键信息:
never是一种"空类型"——它的值域中没有任何合法值存在;never通常是"收窄(narrowing)"的产物——当所有可能的类型分支都被排除后,剩余空间就被推断为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 has type never here because it can never be anything other than a string or a number const neverVal: never = val; console.log(`Unexpected value: ${neverVal}`); } };val的声明类型是联合类型string | number。在if/else if两个分支中,typeof类型守卫(type guard)已经排除了string和number两种可能,因此else分支中val的剩余类型空间为空——编译器将其推断为never。此时把val赋值给一个显式标注为never的变量是合法的,这本身就是编译器对"此处代码不可达"的一种类型级证明。
二、理解前置:联合类型与收窄机制
要理解never为什么会出现,必须先理解它产生的土壤——联合类型与类型收窄。本书 union-type.md 定义:
联合类型(Union Type)是表示"值可以是若干类型之一"的类型,使用
|符号连接各候选类型。
let x: string | number; x = 'hello'; // Valid x = 123; // Valid而 narrowing.md 进一步说明:
TypeScript 收窄是在条件代码块内精化变量类型的过程。在处理联合类型(变量可能具有多个类型)时非常有用。
本书列举了 TypeScript 识别收窄的多种途径:
typeof类型守卫:基于变量的内建 JavaScript 类型进行判断,例如typeof x === 'number'后x被收窄为number;- 真值收窄(truthiness narrowing):通过判断变量是否为真值/假值收窄类型,例如
if (name)排除null与undefined; - 相等性收窄(equality narrowing):配合
switch语句与===、!==、==、!=运算符收窄类型; in运算符收窄:基于对象类型中是否存在某个属性来收窄,例如if ('breed' in pet)区分Dog | Cat;instanceof收窄:基于构造函数判断对象是否为某类实例,例如shape instanceof Square后shape被收窄为Square。
收窄机制的每一次"排除",都在消耗联合类型中的候选成员。当所有候选成员都被消耗殆尽时,剩下的"类型空间"就是never——这正是第一节示例中else分支里val: never的由来。因此可以这样理解:never是收窄链的终点,是"类型空间的空集"。
三、never的底层语义:类型系统的最底层
从类型系统的语义可以推断,never是 TypeScript 的最底层类型(bottom type),它处于所有类型的最底部,具有两条核心性质:
never可以赋值给任何类型:既然never的值域为空,那么"一个空集合的成员"天然满足任何类型约束,赋值永远不会失败;- 任何类型都不能赋值给
never(never本身除外):因为never的值域为空,任何"可能存在的值"都无法放入这个空集合。
正因为如此,never常被用作"不可达代码"的类型标注。当编译器在一个分支中推断出某个表达式为never,却仍然要求它满足某个具体类型时,就会在编译期暴露逻辑漏洞——这为穷尽性检查(见第五节)提供了基础。
需要注意的是,never与unknown正好处于类型谱系的两端:unknown-type.md 指出,unknown代表"未知类型",它要求在使用前必须先做类型检查或断言,且unknown只能赋值给any和unknown自身,是any的类型安全替代品。而never则位于最底部,两者一个代表"不确定是什么",一个代表"什么都不是"。
四、never与void:看似相近,实则完全不同
初学者最容易混淆的就是never与void。本书 void-type.md 的定义是:
void类型用于表示一个函数不返回值。
const sayHello = (): void => { console.log('Hello!'); };二者的本质区别在于:
void是"返回类型"层面的约定:函数正常结束,只是没有返回任何值(返回值实际是undefined);never是"值空间"层面的承诺:函数的执行路径根本不可能正常结束,例如抛出了异常、进入了死循环,或者(结合收窄场景)该分支在类型上就不可能到达。
用一句话概括:void意味着"函数结束但不给值",never意味着"永远没有结束/永远没有值"。这也是为什么never能够承担"不可达代码标记"的职责,而void不能。
五、never的核心实战:穷尽性检查(Exhaustiveness Checking)
never最有价值的应用场景,是配合可辨识联合(Discriminated Unions)与switch/if语句做穷尽性检查。本书 exhaustiveness-checking.md 开宗明义:
穷尽性检查是 TypeScript 的一项特性,用于确保可辨识联合的所有可能情况都在
switch语句或if语句中被处理。
其完整示例为:
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 将抛出错误。
5.1 为什么能"编译期报错"?
当Direction只有'up' | 'down'时,switch的两个case已覆盖全部成员,default分支中direction被收窄为never,将其赋值给const exhaustiveCheck: never是合法的,程序正常编译。
一旦有人把Direction扩展为'up' | 'down' | 'left',default分支中的direction类型就不再是never,而是'left'。此时const exhaustiveCheck: never = direction;就会触发编译错误——"'left'不能赋值给never"。开发者不得不回到switch中补上case 'left'的处理逻辑。错误在编译期暴露,而不是等到运行时悄悄落入 default 分支。
5.2 与可辨识联合结合
本书 discriminated-unions.md 定义了可辨识联合:
可辨识联合(Discriminated Unions)是联合类型的一种,它使用一个公共属性(称为判别属性 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将二者组合,就构成了 TypeScript 社区中最经典的"可扩展类型 + 穷尽处理"模式:新增一种形状(如Triangle)时,area的switch若未覆盖,编译器会在never断言处报错,强制开发者同步更新所有处理逻辑,避免"新增类型却忘记处理"的运行时事故。
5.3 穷尽性检查的常见变体
- 在
if/else链末尾使用never断言:与第一节printValue示例完全一致,else分支中的const neverVal: never = val;即穷尽性断言; - 封装为辅助函数:可以定义一个
assertNever(value: never): never形式的工具函数,在非穷尽时抛错并利用never类型使后续代码不可达,让错误信息更友好; - 作为函数返回值类型:用
: never标注"永不返回"的辅助函数(如抛错函数),让编译器理解调用点之后的代码不可达。
六、控制流分析与never的推断时机
never的推断依赖 TypeScript 的控制流分析(Control Flow Analysis)。本书 control-flow-analysis.md 定义:
TypeScript 的控制流分析是一种静态分析代码流以推断变量类型的方式,编译器可根据分析结果在需要时收窄变量类型。
该章节特别指出一个版本演进事实:
在 TypeScript 4.4 之前,代码流分析只应用于
if语句内的代码;从 TypeScript 4.4 开始,它也可以应用于条件表达式,以及通过const变量间接引用的判别属性访问。
const f1 = (x: unknown) => { const isString = typeof x === 'string'; if (isString) { x.length; } }; const f2 = ( obj: { kind: 'foo'; foo: string } | { kind: 'bar'; bar: number } ) => { const isFoo = obj.kind === 'foo'; if (isFoo) { obj.foo; } else { obj.bar; } };同时,书中也给出了收窄不生效的反例,有助于理解never推断的边界条件:
const f1 = (x: unknown) => { let isString = typeof x === 'string'; if (isString) { x.length; // Error, no narrowing because isString it is not const } }; const f6 = ( obj: { kind: 'foo'; foo: string } | { kind: 'bar'; bar: number } ) => { const isFoo = obj.kind === 'foo'; obj = obj; if (isFoo) { obj.foo; // Error, no narrowing because obj is assigned in function body } };由此可知,never的推断并非"魔法":只有当收窄条件是稳定且可追踪的(如const绑定的判断结果、未被重新赋值的变量),控制流分析才能把剩余类型空间精确收窄到never。书中还提示:条件表达式中的间接引用最多分析五层。理解这些边界,有助于避免写出"该收窄却没收窄"的代码。
七、与类型谓词、unknown的配合使用
never还经常出现在类型守卫相关的场景中。本书 type-predicates.md 介绍类型谓词(Type Predicates):
类型谓词是返回布尔值的函数,用于将变量的类型收窄到更具体的类型。
const isString = (value: unknown): value is string => typeof value === 'string'; const foo = (bar: unknown) => { if (isString(bar)) { console.log(bar.toUpperCase()); } else { console.log('not a string'); } };该章节还记录了 TypeScript 5.5 的重要改进:
TypeScript 5.5 会在
.filter等函数中自动推断类型谓词(如x is T),从而知道undefined等值何时被移除——带来更精确的类型和更少的错误;这适用于清晰的检查(如x !== undefined),但不适用于!!x这类含糊的写法。
const nums = [1, null, 2].filter(x => x !== null);在 5.5 之前,filter返回的数组类型不会剔除null;5.5 之后,类型系统自动推断出x is T形式的谓词,nums被推断为number[]。这与never的关系在于:类型谓词的else分支、以及"剔除后剩余的联合成员"最终都会归结到never的推断上——当类型谓词把联合中的所有候选成员都判定掉,剩余空间就是never。
在防御式编程中,一个常见的组合是:函数参数声明为unknown(强制先收窄再使用,见 unknown-type.md),内部用typeof守卫逐层收窄,最后在else分支用never断言兜底——这既保证了运行时安全,也让所有未预期的输入在编译期被标记为不可达。
八、never使用自查清单与常见误区
结合以上章节,整理一份实战自查清单:
else分支出现never,是好事:说明你的类型守卫已经覆盖了所有分支,代码是穷尽的;never不等于void:函数不返回值用void;永远不结束/不可能到达的路径才用never;- 穷尽性检查记得用
never断言:在switch的default或if/else的收尾分支写上const exhaustiveCheck: never = value;,让新增联合成员时编译报错; - 收窄条件的稳定性影响推断:优先使用
const保存判断结果,避免在函数体内重新赋值目标变量,否则控制流分析无法收窄到never; never与unknown是两端:unknown表示"未知但有值",never表示"确定没有值",不要把两者混用。
常见误区提醒:不要试图"主动使用"never作为常规变量的类型注解(因为它不能容纳任何值);never的价值不在于被显式写出,而在于编译器在穷尽判断、不可达代码检测中的自动化应用——你在代码中做的最重要的一件事,是为never的出现创造条件(完整覆盖联合成员的分支 + 稳定的收窄条件 + 穷尽性断言),其余交给类型系统。
九、小结
never是 TypeScript 收窄机制的自然终点:联合类型的候选成员被逐一排除后,剩余空间必然是never;而 exhaustiveness-checking.md 所示的"default 分支 +never断言"模式,则把它从"类型系统的理论概念"变成了"编译期防御机制"——联合类型每新增一个成员,未处理的分支就会立刻报错。掌握never,意味着你不仅理解了 TypeScript 的类型收窄,还掌握了让类型系统替你守住"所有情况都被处理"这条底线的工程化方法。建议继续阅读仓库中的 narrowing.md、discriminated-unions.md 与 control-flow-analysis.md 章节,把never放进完整的类型收窄体系中理解。
- 文档
- 教程
【免费下载链接】typescript-book
The Concise TypeScript Book: A Concise Guide to Effective Development in TypeScript. Free and Open Source.
相关推荐
The Concise TypeScript Book 精读:never 类型从类型收窄到穷尽性检查的完整指南
The Concise TypeScript Book 精读:never 类型从类型收窄到穷尽性检查的完整指南 本篇围绕开源项目 The Concise Typ
文档教程The Concise TypeScript Book 中的 never 类型:从空集合收窄到穷尽性检查
The Concise TypeScript Book 中的 never 类型:从空集合收窄到穷尽性检查 本篇围绕 The Concise TypeScript
文档教程The Concise TypeScript Book 精读:深入理解 never 类型与穷尽性检查
The Concise TypeScript Book 精读:深入理解 never 类型与穷尽性检查 never 类型是 TypeScript 类型系统中一个看
文档教程
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考