前言
最近完整地看了一遍TypeScript
的官方文档,发现文档中有一些知识点没有专门讲解到,或者是讲解了但却十分难以理解,因此就有了这一系列的文章,我将对没有讲解到的或者是我认为难以理解的知识点进行补充讲解,希望能给您带来一点帮助。
tips:配合官方文档食用更佳
这是本系列的第一篇TypeScript中never的正确打开方式
,在TypeScript
存在一个类型never
,其表示用不存在的值的类型,看上去一脸懵逼,不存在的值还要什么类型???接下来让我们揭开never
的神秘面纱,获得never
的正确打开方式。
什么是never
类型
never
类型是TypeScript2.0
引入的一个新的原始类型。其表示的是那些永不存在的值的类型。
never
类型的特征
never
类型是所有类型的子类型,即never
类型可以赋值给任何类型。- 其他任何类型均不是
never
类型的子类型,即其他类型均不可赋值给never
类型,除了never
本身。即使any
类型也不可以赋值给never
类型。 - 返回类型为
never
的函数中,其终点必须是不可执行的,例如函数过程中抛出了错误或者存在死循环。 - 变量也可以声明为
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
(官方文档指出可以为其赋予null
或undefined
,但经本地测试,即使没有打开tsconfig中
的strictNullChecks
,null
也不可以赋给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
类型,然后接口IPersonAnyAttr
将age
属性设置为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