news 2026/9/25 6:01:22

TypeScript `never` 类型完全指南:从收窄推断到穷尽性检查(The Concise TypeScript Book 深度解读)

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
TypeScript `never` 类型完全指南:从收窄推断到穷尽性检查(The Concise TypeScript Book 深度解读)
  • 文档
  • 教程

【免费下载链接】typescript-book

The Concise TypeScript Book: A Concise Guide to Effective Development in TypeScript. Free and Open Source.

项目地址:https://gitcode.com/gh_mirrors/typ/typescript-book
点击查看免费下载

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类型代表一个永远不可能被产生的值。

这段话包含两个关键信息:

  1. never是一种"空类型"——它的值域中没有任何合法值存在;
  2. 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使用自查清单与常见误区

结合以上章节,整理一份实战自查清单:

  1. else分支出现never,是好事:说明你的类型守卫已经覆盖了所有分支,代码是穷尽的;
  2. never不等于void:函数不返回值用void;永远不结束/不可能到达的路径才用never;
  3. 穷尽性检查记得用never断言:在switch的default或if/else的收尾分支写上const exhaustiveCheck: never = value;,让新增联合成员时编译报错;
  4. 收窄条件的稳定性影响推断:优先使用const保存判断结果,避免在函数体内重新赋值目标变量,否则控制流分析无法收窄到never;
  5. 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.

项目地址:https://gitcode.com/gh_mirrors/typ/typescript-book
点击查看免费下载
上一篇:Kotaemon 文档聊天故障排除完整指南:从启动失败到聊天无响应的分层排查法
下一篇:Notepad4高级功能详解:正则表达式搜索、Base64编解码与API列表的终极指南 🚀

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/25 6:00:38

智慧社区管理系统毕设实战:Spring Boot+Vue全栈开发与避坑指南

简介:这份资源是面向高校计算机相关专业学生与课程设计学习者的「小康之家智慧社区管理系统」毕业设计完整源码包,围绕现代社区数字化管理场景,覆盖居民信息管理、物业缴费、社区公告、报修服务、智能安防、活动报名、数据报表与用户权限等核…

作者头像 李华
网站建设 2026/9/25 5:59:57

OpenSSL在64位系统下的lib/dll与头文件配置指南

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/25 5:59:23

使用 AWS SDK for Java 2.x 操作 IAM 的完整实践指南

示例工程教程后端 【免费下载链接】aws-doc-sdk-examples Welcome to the AWS Code Examples Repository. This repo contains code examples used in the AWS documentation, AWS SDK Developer Guides, and more. For more information, see the Readme.md file below. 项目地…

作者头像 李华
网站建设 2026/9/25 5:59:20

Atlas 300V上部署YOLO:从环境搭建到推理调优实战指南

1. Atlas 300V到底是个什么卡1.1 一个最容易被搜到的问题如果你是因为“atlas部署yolo”或者“atlas 300v 24g 是运算加速卡吗”这种问题点进来的,那我的答案是:是的,Atlas 300V 24G就是一块专用的AI运算加速卡,但它的定位非常明确…

作者头像 李华
网站建设 2026/9/25 5:58:39

SCARA机械臂ROS运动规划避坑指南:URDF校准、IKFast定制与ROS1/2双版本控制

简介:本资源是一套面向ROS开发者与机器人控制学习者的SCARA机械臂运动规划与控制集成包,聚焦工业自动化场景下高精度、高速度装配任务的算法实现与工程落地。资源完整支持ROS1与ROS2双版本,基于MoveIt框架实现逆运动学解析、平滑轨迹规划及实…

作者头像 李华