TypeScript中never的正确打开方式

1,035 阅读5分钟

前言

最近完整地看了一遍TypeScript的官方文档,发现文档中有一些知识点没有专门讲解到,或者是讲解了但却十分难以理解,因此就有了这一系列的文章,我将对没有讲解到的或者是我认为难以理解的知识点进行补充讲解,希望能给您带来一点帮助。

tips:配合官方文档食用更佳

这是本系列的第一篇TypeScript中never的正确打开方式,在TypeScript存在一个类型never,其表示用不存在的值的类型,看上去一脸懵逼,不存在的值还要什么类型???接下来让我们揭开never的神秘面纱,获得never的正确打开方式。

什么是never类型

never类型是TypeScript2.0引入的一个新的原始类型。其表示的是那些永不存在的值的类型。

never类型的特征

  1. never类型是所有类型的子类型,即never类型可以赋值给任何类型。
  2. 其他任何类型均不是never类型的子类型,即其他类型均不可赋值给never类型,除了never本身。即使any类型也不可以赋值给never类型。
  3. 返回类型为never的函数中,其终点必须是不可执行的,例如函数过程中抛出了错误或者存在死循环。
  4. 变量也可以声明为never类型,但其不能被赋值。(不能赋值声明这个变量干什么?当赋值时就会出现错误,可用于提前发现错误,如下面场景三)

never的典型使用场景

场景一:终点永远不会被执行的函数

function neverReturnFunc1():never{
    throw new Error("Error Message")
    return "i can't be reached"
}
// 这时由于函数中抛出了错误,其返回字符串的语句永远不可能被执行,因此其函数类型为:()=>never
function neverReturnFunc2():never{
    while(true){
        //....
    }
    return "i can't be reached"
} 
// 这时由于函数中出现了死循环,其返回字符串的语句永远不可能被执行,因此其函数类型也是:()=>never

never类型与void类型的区别:

never类型表示永不存在的值的类型。

void表示没有任何类型,只能为其赋予undefined(官方文档指出可以为其赋予nullundefined,但经本地测试,即使没有打开tsconfig中strictNullChecksnull也不可以赋给void类型)。

当一个函数没有返回值(即隐式返回undefined),而不是到达不了返回值的语句时,其返回类型为void

function voidReturnFunc():void{
    console.log("i don't have return value")
}
// 这时voidReturnFunc的类型为:()=> void;

场景二:一个特殊的类型

问题:对于一个接口,如何定义其某个属性为number类型,其他不确定的属性都为string类型。

在解决这个问题时,never类型便可以派上用场。

首先展示一种常见的错误的写法:

interface IPerson {
    age: number;
    [key: string]: string;
}
//error:Property 'age' of type 'number' is not assignable to 'string' index type 'string'.

其中age属性是number类型,这与string索引签名的string类型是不兼容的,因此是错误的。

可以通过类型联合以及never类型实现。

interface IPersonAge{
    age:number;
}
interface IPersonAnyAttr{
    age:never;
    [key:string]:string;
}
type PersonType=IPersonAge | IPersonAnyAttr

首先接口IPersonAge定义了age属性为number类型,然后接口IPersonAnyAttrage属性设置为never类型,由于never类型是任何类型的子类型,因此是不会和下面的索引签名的string类型冲突的,并且经过类型联合也是不会冲突的,因此通过类型联合将age属性扩张为number类型,就可以实现我们想要的类型。

场景三:可辨识联合的完整性检查

可辨识联合:可以简单的理解为多个类型的联合,并且每个类型有可以辨识的特征。

interface Square {
    kind: "square";
    size: number;
}
interface Rectangle {
    kind: "rectangle";
    width: number;
    height: number;
}
interface Circle {
    kind: "circle";
    radius: number;
}
type Shape = Square | Rectangle | Circle;

其中Shape类型就是一个可辨识联合,其中多种类型进行联合,并且每个接口都有kind属性并且有着不同的字符串字面量类型,是可辨识的,kind属性一般被称作可辨识的特征或标签。

接下来就可以利用可辨识的特征来使用可辨识联合,这里我们通过kind来计算不同形状的面积。

type Shape = Square | Rectangle | Circle;
function area(s: Shape) {
    switch (s.kind) {
        case "square": return s.size * s.size;
        case "rectangle": return s.height * s.width;
        case "circle": return Math.PI * s.radius ** 2;
    }
}

如果我们想涵盖所有的case,当没有全部覆盖时,如何让编译器可以通知我们,比如我们在Shape类型里添加了Triangle类型。

type Shape = Square | Rectangle | Circle | Triangle;

这时我们就可以借助never类型进行完整性检查。

function assertNever(x: never): never {
    throw new Error("Unexpected object: " + x);
}
function area(s: Shape) {
    switch (s.kind) {
        case "square": return s.size * s.size;
        case "rectangle": return s.height * s.width;
        case "circle": return Math.PI * s.radius ** 2;
        default: return assertNever(s); 
        // error:Arguments of type 'Triangle' is not assignable to parameter of type 'never'
    }
}

当case中没有涵盖所有的情况时,就会走到default选项,这时,传入的s值不是never类型,这时便会出现编译错误,从而实现对联合类型的完整性检查。

场景四:实现Exclude类型以及Extract类型

Exclude<T, U> -- 从T中剔除可以赋值给U的类型。

Extract<T, U> -- 提取T中可以赋值给U的类型。

通过实际例子来理解这两个类型:

type T1 = "a" | "b" | "c" | "d";  
type T2 = "a" | "c" | "f"
type ExcludeT1T2=Exclude<T1,T2> //"b"|"d"
type ExtractT1T2=Extract<T1,T2> //"a" | "c"

首先通过never实现一下Exclude

type Exclude<T, U> = T extends U ? never : T;

T为联合类型时,会自动分发条件,对T中的所有类型进行遍历,判断其是否是U的子类,如果是的话便返回never类型,否则返回其原来的类型。最后再将其进行联合得到一个结果联合类型。

由于never类型与其他类型联合最终得到的还是其他类型,因此便可以从类型T中剔除掉可以赋给U的类型。

同理,将never类型和T类型互换,便可实现Extract

type Extract<T, U> = T extends U ? T : never;

结语

本人也是在学习的过程中边学习、边记录、边分享,文章中不免有描述不恰当或是错误的地方,如若发现,请您评论指正!

也希望可以和大家共同交流,共同成长。

we_chat:Kenny-Shaw