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
void | never | |
|---|---|---|
| 函数会结束 | 是 | 否(抛出异常或无限循环) |
| 运行时的值 | 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;加上类型注解,或者修正相互冲突的类型。