「TypeScript 奇淫巧技」never 的隐藏特性!

983 阅读3分钟

先来复习一下 never 的基本概念。

程序语言的设计确实应该存在一个底部类型的概念,当你在分析代码流的时候,这会是一个理所当然存在的类型。TypeScript 就是这样一种分析代码流的语言(😎),因此它需要一个可靠的,代表永远不会发生的类型。

never 类型是 TypeScript 中的底层类型。它自然被分配的一些例子:

  • 一个从来不会有返回值的函数(如:如果函数内含有 while(true) {});
  • 一个总是会抛出错误的函数(如:function foo() { throw new Error('Not Implemented') }foo 的返回类型是 never);

你也可以将它用做类型注解:

let foo: never; // ok

但是,never 类型仅能被赋值给另外一个 never

let foo: never = 123; // Error: number 类型不能赋值给 never 类型

// ok, 作为函数返回类型的 never
let bar: never = (() => {
  throw new Error('Throw my hands in the air like I just dont care');
})();

细节一

never 是所有类型的子类型,也就是说never类型可以赋给所有类型,但是反之任何类型都不能赋给 never

let a: never = (function () {
    throw new Error('Not Implemented')
})();

let b: number = a;

上面的例子里,虽然 let b 这行的代码永远也不会执行到,但是这样确实不会触发类型错误,你可能会问这有任何意义么?

事实上是有的,never 是所有类型的子类型也就是说所有类型都是 never的逆变,众所周知涉及函数兼容性的时候,参数是逆变的,所以使用 ...arg: never[] 作为参数可以做到对任何参数的兼容:

type MyReturnType<T> = T extends (...args: never[]) => infer Return
    ? Return
    : never;

细节二

never 进行高级操作的时候产生的衍生类型可能还是 never,这个我没找到具体的文档说明,在实践中发现了这几个:

type stillNever = [1, ...never];
// 对 never 进行展开;
type stillNever = never & 1;
// 对 never 进行并集;
type Never = never;
type stillNever = `${Never}`;
// 在模板字符串中使用 never;

在泛型编程中使用递归的时候一定要避免返回 never,可能会和你使用的泛型操作符产生上面的副作用。

细节三

never 在碰上联合类型的时候会自动砍掉自己:

type One = never | 1;
// 1

这个特性有的时候还挺好用的,其实我感觉 never 本身也可以看做一种联合类型,就是应为这个特性才会产生下面的细节四。

细节四

使用 never 作为泛型参数的时候产生了条件类型的分发机制,生成的类型不论条件类型返回值如何都是 never

type IsNumber<T> = T extends number ? true : false;
type stillNever = IsNumber<never>; 

这个特性只针对产生条件分发的情况,使用 never 作为泛型参数本身不会产生问题:

type OrOne<T> = T | 1;
type A = OrOne<never>; 
// 1

所以我们想判断一个类型是不是 never 类型的时候要记得阻止条件分发机制:

export type IsNever<T> = [T] extends [never] ? true : false;

void 的差异

一旦有人告诉你,never 表示一个从来不会优雅的返回的函数时,你可能马上就会想到与此类似的 void,然而实际上,void 表示没有任何类型,never 表示永远不存在的值的类型。

当一个函数返回空值时,它的返回值为 void 类型,但是,当一个函数永不返回时(或者总是抛出错误),它的返回值为 never 类型。void 类型可以被赋值(在 strictNullCheckingfalse 时),但是除了 never 本身以外,其他任何类型不能赋值给 never