Menu

TypeScript never 类型:穷尽检查与 never 和 void 的区别

never 是没有任何值的类型。它是永远不会结束的函数的返回类型,是类型收窄排除了所有情况后剩下的类型,也是 switch 穷尽检查背后的工具。本文讲解 never 从哪里来,以及它和 void 有什么不同。

本页包含可运行的编辑器。编辑、运行,立即查看输出。

never 是没有任何值的类型。返回类型为 never 的函数永远不会正常返回:它要么抛出异常,要么永远运行下去。never 类型的变量标记着不可能执行的代码,正是这一点让穷尽检查成为可能。

在返回 number 的函数中,return fail(...) 可以编译通过,因为 never 可以赋值给任何类型:never 类型的值永远不会真正出现。

永不返回的函数

有两种函数永远到达不了结尾:总是抛出异常的函数,以及带无限循环的函数。编译器会核实这个声明。标注为 never 的函数如果结尾可达,会报错 TS2534:A function returning 'never' cannot have a reachable end point。

function fail(message: string): never {
  throw new Error(message);
}

function runForever(): never {
  while (true) {
    // poll, serve requests...
  }
}

推断结果因语法而异。只抛出异常的函数声明会被推断为返回 void,而只抛出异常的箭头函数或函数表达式会被推断为 never:

function f1() { throw new Error("x"); }       // () => void
const f2 = () => { throw new Error("x"); };   // () => never

只有当被调用的名字带有返回 never 的显式类型时,类型收窄才会把这次调用视为死路:像上面的 fail 那样标注了 : never 的函数声明,或者带类型注解的变量 const fail: (m: string) => never = (m) => { throw new Error(m); }。推断出的 never 不算数,const fail = (m: string): never => ... 也不算,因为这里只标注了箭头函数,而没有标注变量。

never 与 void

voidnever
函数会结束是否(抛出异常或无限循环)
运行时的值undefined没有:调用永远不会产生值
调用之后的代码可达不可达
可以赋值给其他类型只能赋值给 void、unknown、any任何类型
典型用途回调、事件处理函数、有副作用的函数fail()、assertNever()、无限循环

实际的区别体现在类型收窄上。在 if (!user) fail("no user") 之后,只有当 fail 返回 never 时,编译器才知道下一行的 user 已经有定义。如果返回 void,编译器会认为执行可以继续。

用 never 做穷尽检查

对联合类型做 switch 时,每个 case 都会收窄这个值。当所有成员都处理完后,default 中剩下的就是 never。把它赋给一个 never 变量,“我处理了所有情况”就变成了编译器会验证的事情:

现在给联合类型加上第三个成员,但不加对应的 case:

index.ts(18,26): error TS2345: Argument of type '{ kind: "triangle"; base: number; height: number; }' is not assignable to parameter of type 'never'.

错误信息会指出你漏掉的成员。加上 case "triangle": return (shape.base * shape.height) / 2;,代码就又能编译通过了。assertNever 辅助函数是同一检查的可复用形式,它的 throw 在运行时仍然有用:来自 JSON 或旧版客户端的数据可能包含类型上认为不可能出现的 kind。这个模式是 可辨识联合类型 的核心。

收窄到 never

任何类型收窄都会发生同样的事情,不只是 switch。一旦所有可能都被排除,变量的类型就是 never:

如果以后把参数放宽为 string | number | boolean | bigint,const nothing: never = x 这一行就会报错,指出需要更新的函数。

never 在联合类型中会消失

never 是值的空集,所以把它加进联合类型不会改变任何东西:string | never 就是 string。条件类型正是这样过滤联合类型的。返回 never 的分支会把那个成员移除:

内置的 Exclude<T, U> 和 Extract<T, U> 就是这样工作的。在交叉类型中则正好相反:string & never 是 never。

不可能的类型会变成 never

没有任何值能满足的交叉类型会化简为 never:

type A = string & number;                   // never
type B = { kind: "a" } & { kind: "b" };     // never

读取 B 值的属性时,错误信息会说明原因:Property 'kind' does not exist on type 'never'. The intersection 'B' was reduced to 'never' because property 'kind' has conflicting types in some constituents. 如果你构建的类型变成了 never,就去找找有哪两个部分相互矛盾。

never、unknown 与 any

类型包含的值可以赋值给接受
unknown所有值只有 unknown 和 any一切
any所有值除 never 以外的一切一切
never没有值一切只有 never

unknown 是类型层级的顶端,never 是底端。any 根本不属于这个层级:它会关闭类型检查。

常见问题

TypeScript 中的 never 类型是什么?

never 是没有任何值的类型。除了另一个 never,什么都不能赋值给它,而它可以赋值给任何类型。它会作为总是抛出异常或无限循环的函数的返回类型出现,作为类型收窄排除了所有可能之后变量的类型出现,也会作为 string & number 这类不可能类型的结果出现。

never 和 void 有什么区别?

返回 void 的函数会正常结束,只是不返回有用的值(运行时返回 undefined)。返回 never 的函数根本不会结束:它要么抛出异常,要么永远运行下去。调用 never 函数之后的代码是不可达的,TypeScript 在类型收窄时也会这样处理。

TypeScript 中如何做穷尽检查?

在对联合类型做 switch 的 default 分支中,把值赋给一个 never 类型的变量,或者传给一个会抛出异常的函数 assertNever(value: never): never。如果所有情况都处理了,那里的值就是 never,代码可以编译通过。如果漏掉了某种情况,编译器会报告漏掉的成员不能赋值给 never。

为什么我的类型变成了 never?

通常是因为 TypeScript 把所有选项都收窄掉了(例如对 string | number 先后检查了 typeof x === "string" 和 typeof x === "number"),或者因为交叉类型不可能成立:string & number,或者两个对象类型的同名属性有相互冲突的字面量类型。在编辑器里把鼠标悬停在类型上,看看是哪一步产生了它。

“is not assignable to type never”是什么意思?

代码试图把一个真实的值放到只允许 never 的地方。在穷尽检查中,这表示有一个联合类型成员没有被处理。在其他地方,它常常意味着某个数组被推断成了 never[],或者某个交叉类型坍缩成了 never;加上类型注解,或者修正相互冲突的类型。

Coddy 编程语言插图

和 Coddy 一起学编程

开始