本文主要参考了typescript官网的TS指南,然后加上自己刷type-challenge(类型体操)的感受总结写的进阶指南。并不是官网指南的翻译版。
啰嗦两句:如果你连什么是接口、类型和泛型这些基础知识都不知道的话,就没必要往下了;如果类型体操你做easy以上都毫无压力也可以不用往下了。
有写得不对或者不清楚的地方,请评论告诉我。
1.泛型
泛型是什么
首先,我们写一个identity
函数,它的作用是你传入什么参数就返回什么参数。如果不使用泛型的话,我们可以这样写:
// 这种写法只能满足arg为number的情况
function identity(arg: number): number {
return arg;
}
// 换种写法,使用any
function identity(arg: any): any {
return arg;
}
复制代码
虽然使用any
类型可以实现identity
的基本功能,因为它会导致函数接受 arg
类型的任何类型,但是当函数返回时,我们实际上丢失了关于该类型是什么的信息。如果我们传入一个数字,那么我们得到的唯一信息就是任何类型都可以返回,所以也是不完美的。
下面使用泛型来实现:
function identity<Type>(arg: Type): Type {
return arg;
}
复制代码
这个版本的identity
函数就符合开头说的输入任意参数后原封不动的返回参数,也就是恒等函数。<Type>
的作用就是去捕捉输入参数的类型。一般泛型是用T/U/V/P之类的命名,实际项目中如果多人开发可以将泛型命名得更具体一些比如:Input
、UISchema
等等。
泛型函数/接口
当定义了以下泛型函数声明:
const myIdentity: <Type>(arg: Type) => Type;
复制代码
此时定义myIdentity
函数的形式,只有以下函数才可以赋值给myIdentity
:
function identity<T>(arg:T):T {
return arg
}
复制代码
泛型还可以用在interface(接口)上:
// 一般接口这么写
interface GenericIdentityFn {
arg: number;
}
// 泛型接口
interface GenericIdentityFn<Type> {
(arg: Type): Type;
}
let myIdentity: GenericIdentityFn<number> = identity;
复制代码
可以看出,上面的例子将泛型参数作为整个接口的参数。这让我们看到myIdentity
的泛型最终是什么类型的(例如GenericIdentityFn<number>
而不仅仅是GenericIdentityFn
)。这使得类型参数对阅读接口的其他人的可读性大大增加。
2.类型操作符keyof
在JS里实现在obj里返回某个key的value,通常会这么写:
function getOneObjValue (obj, key) {
return obj[key]
}
复制代码
但是其实这个函数有漏洞,如果输入的key不属于obj的话,函数就会返回undefined
,可能导致后续代码出错。一般JS处理的方式是在函数里写if else把边缘case过滤掉,日积月累函数里可能有各种各样的if else相互嵌套,特别恶心。
在TS里的话,我们需要给函数加上类型:
function getOneObjValue (obj: T, key: keyof T):T[keyof T] {
return obj[key]
}
复制代码
这里的keyof T
表示对T进行索引遍历查询,得到的类型是T的键名组成的模板字面量类型,此时的key的取值范围就被锁定为T中的key。如果输入不属于T的键名时,TS会报错。函数的返回值类型为T[keyof T]
(概念名为索引访问),也是类似JS里取对象的value时使用obj[key]
一样。由于keyof自带遍历的特性,并不需要去写for循环依次取出,类似于JS里的object.keys()
。
举个例子:
// 定义一个接口
interface Person {
"name": string,
"age": number
}
type p1 = keyof Person; // "name" | "age"
type p2 = Person["name"]; //p2的类型为 string
type p3 = Person[keyof Person];//p3的类型为 string | number
复制代码
下面穿插着看看模版字面量类型是什么:
// 模板字面量的语法和JS的模板字符串差不多
type EmailLocaleIDs = "welcome_email" | "email_heading";
type FooterLocaleIDs = "footer_title" | "footer_sendoff";
type AllLocaleIDs = `${EmailLocaleIDs | FooterLocaleIDs}_id`;
// 此时的AllLocaleIDs的类型显示为:
type AllLocaleIDs = "welcome_email_id" | "email_heading_id" | "footer_title_id" | "footer_sendoff_id"
复制代码
为了贯彻泛型,其实getOneObjValue
函数还有改进的空间。keyof
在函数中重复了两次,我们可以用一个泛型类型变量来代替它,下面才是最终我们会写出来的形式:
function getOneObjValue (obj: T, key: keyof T):T[keyof T] {
return obj[key]
}
// 最终版
function getOneObjValue<T extends object, U extends keyof T> (obj: T, key: U):T[U] {
return obj[key]
}
复制代码
首先我们一般会使用T/P/U/V去表示一个泛型变量,那extends
是什么意思呢?在TS里,像这样的类型编程里的extends和JS语言中类subclass extends class
不是同一个extends,这里的A extends B
可以简单理解为前者被后者约束或者是前者是后者的子集。在T extends object
里object
是基本类型之一,也就是对象,所以可以理解为泛型T是对象的子集或者T被限制为对象类型,类似的U也好理解了。
下面,假设此时不是去obj取一个值,而是取一组值,函数应该修改为如下:
function getOneObjValue<T extends object, U extends keyof T> (obj: T, key: U):T[U] {
return obj[key]
}
// 数组
function getObjValueArr
<T extends object, U extends keyof T> (obj: T, keys: U[]):T[U][] {
return keys.map((key) => obj[key]);
}
复制代码
主要是将传入参数keys改为数组类型keys: U[]
,然后输出值的类型改为数组T[U][]
。这里的基础知识在于:
// 定义number类型
let a:number = 1;
// 定义数组类型
let arr1:number[] = [1,2,3];
复制代码
3.类型操作符typeof
TS里typeof
的用法和JS里的typeof
差不多,看下面的对比:
// JS
console.log(typeof "Hello world"); // 输出string
//TS
let s = "hello";
let n: typeof s; // 表示n的类型是string
复制代码
typeof
会取后面变量的类型,但是注意这里的变量是类型变量(Type!),在TS的类型编程里并不能把非类型变量(值!)用在类型操作符里。
举个例子就明白了:
首先,我们来写一个函数类型(function type),也就是对一个变量进行函数定义的类型约束:
// Predicate是一个函数定义
type Predicate = (x: unknown) => boolean;
// ReturnType是TS的内置功能类型,可以直接用,它的作用是取函数类型返回值的类型
type K = ReturnType<Predicate>; // 此时K的类型是boolean
复制代码
然后,我们再来写一个函数,然后把ReturnType
用在函数名上:
function f() {
return { x: 10, y: 3 };
}
// 会报错,因为类型操作符只能操作类型
type P = ReturnType<f>;
// 利用typeof取类型
type P = ReturnType<typeof f>; // 此时 type P = { x:number; y:number }
复制代码
对于typeof
也可以用在下面的情况:
const MyArray = [
{ name: "Alice", age: 15 },
{ name: "Bob", age: 23 },
{ name: "Eve", age: 38 },
];
//此时 type Person = {name: string;age: number;}
type Person = typeof MyArray[number];
//此时 type Age = number
type Age = typeof MyArray[number]["age"];
// 也可以这样取number类型
type Age2 = Person["age"];
复制代码
Person["age"]
又是一个新的知识点,叫作索引访问类型(Index Access Type)。在TS里,我们可以使用一个索引访问类型来查找另一个类型上的特定属性:
type Person = { age: number; name: string; alive: boolean };
type Age = Person["age"]; //此时Age的类型为number
复制代码
有点类似于JS中取对象的值obj[key],在TS里则是取接口属性的类型定义。MyArray[number]
表示取数组里所有的元素值,但是我们需要是是所有元素的类型,就在前面加上typeof
就搞定了。typeof MyArray[number]["age"]
表示在所有元素里取索引为"age"
的类型。
4.条件类型
条件类型是什么
在JS里有if else,在TS里没有。TS的条件类型有点类似于JS的条件运算符..?..:
。
SomeType extends OtherType ? TrueType : FalseType;
复制代码
这个通常可以被用在函数重载上。不过如果不理解函数重载是什么的话没关系,看了下面的例子就懂了:
interface IdLabel {
id: number;
}
interface NameLabel {
name: string;
}
function createLabel(id: number): IdLabel;
function createLabel(name: string): NameLabel;
function createLabel(nameOrId: string | number): IdLabel | NameLabel;
function createLabel(nameOrId: string | number): IdLabel | NameLabel {
throw "unimplemented";
}
复制代码
createLabel
函数需要根据入参的不同产生不同的输出,这就叫函数重载。如果没有条件类型的话,我们需要一直重复定义出每一种场景下函数所需要的类型,无脑重复对于程序员来说无疑是一件不好的事。
仔细看createLabel
函数其实是有三种重载场景id
、name
和nameOrId
,对于前两个类型是确定的返回值类型,对于nameOrId
类型不确定输入的时候,函数返回值是IdLabel
或者NameLabel
。所以此时我们使用条件类型定义一个type来描述函数的返回值定义:
type NameOrId<T extends number | string> = T extends number
? IdLabel
: NameLabel;
复制代码
如果T的类型被约束为number
就返回IdLabel
,否则就返回NameLabel
。
然后,我们可以使用该条件类型将重载简化为单个函数,而不需要重载:
function createLabel<T extends number | string>(idOrName: T): NameOrId<T> {
throw "unimplemented";
}
let a = createLabel("typescript"); // a:NameLabel
let b = createLabel(2.8); // b:IdLabel
let c = createLabel(Math.random() ? "hello" : 42); // c: NameLabel | IdLabel
复制代码
条件类型约束(Conditional Type Constraints)
条件类型约束可以使得推断出的类型更具体,就像使用类型守卫
(后面讲)进一步缩小(收窄narrowing)类型范围可以给我们提供更具体的类型一样,条件类型里为true的那部分要执行的代码将进一步约束泛型。所以产生了条件类型约束的概念。
看下面的例子:
type MessageOf<T> = T["message"]; // 报错:Type '"message"' cannot be used to index type 'T'.
复制代码
报错是因为此时的泛型T上并不一定存在message
,要等用的人输入之后才知道有没有,所以我们需要约束一下T
让代码不要报错。
一般情况下,不用条件类型约束时,大家会这样写:
type MessageOf<T extends { message: unknown }> = T["message"];
// 测试一下
interface Email {
message: string;
}
type EmailMessageContents = MessageOf<Email>; // EmailMessageContents:string
复制代码
但是,如果我们希望 MessageOf
可以输入任意类型而不是限制于{ message: unknown }
,并且在message
属性不可用的情况下默认类型为 never
,我们可以通过移除约束并引入条件类型来实现。
捋一捋:先判断泛型T是不是被{ message: unknown }
约束,是的话就执行T["message"]
,不是的话就将默认为never类型。这不纯纯的条件类型:
type MessageOf<T> = T extends { message: unknown } ? T["message"] : never;
interface Email {
message: string;
}
interface Dog {
bark(): void;
}
//type EmailMessageContents = string
type EmailMessageContents = MessageOf<Email>;
//type DogMessageContents = never
type DogMessageContents = MessageOf<Dog>;
复制代码
修改后的MessageOf
比之前的更直观。
学会类型约束之后来做个小练习,写一个针对数组的Flatten
类型,它的作用是将数组里的元素的类型提取出来,不是数组类型就直接返回当前的类型。又涉及到了是与不是,就要想到条件约束是不是可用:
type Flatten<T> = T extends any[] ? T[number] : T;
//也可以这样写any[]和Array<any>是两种表示数组的方法
type Flatten<T> = T extends Array<any> ? T[number] : T;
// 测试
type Str = Flatten<string[]>; // Str:string
type Num = Flatten<number>;// Num:number
复制代码
这里的T[number]
也就是我们前面讲过的索引访问类型,对于数组泛型来说T[number]
表示取数组所有的元素,因为数组的索引是数字,number
类型囊括了所有数字,这样也就取到了所有元素。
infer 关键字
上面的Flatten
还有进步空间,至于为什么,等我们先学完infer
这个关键字的用法再说。条件类型为我们提供了一种使用 infer 关键字从true的分支代码中得到类型结果的方法。(也就是说infer是只能用在条件类型中的)。
先看语法:
infer T;
复制代码
infer T
中的T
一定要是一个待推断的类型变量,合在一起表示对T进行推断。
Flatten
函数还可以这样写:
type Flatten<Type> = Type extends Array<infer Item> ? Item : Type;
复制代码
这里的Item
表示待推断的类型,此时的定义情况下是不知道Item
到底是什么类型的。这个比上一个版本好在我们不需要通过索引访问类型的方式T[number]
去思考如何取到数组的每一个元素的类型,引入一个新的Item
泛型变量直接表示未来待传入(推断)数组元素的类型。
很多人对infer这个关键词可能会有点摸不着头脑,这里再啰嗦一下:
首先,infer是只服务于条件类型的。
其次,条件类型的表现就是根据输入的泛型条件将输出的类型的范围缩小,也就是说输入的类型是不确定的。对待不确定项,我们要么自己去推断:类似于Flatten
函数里不知道输入数组的每个元素的类型,我们就用T[number]
的方式取出每个元素类型;要么就引入新的泛型变量(类似于Item
)去表示那些未知的元素类型,但是在前面必须加infer
才能够说明Item
是未知的带推断项。简单来说就像在JS里申明一个常量前面一定是加const
才能够说明后面的变量是常量。
最后,实在不懂死记一下用法:infer只用在条件类型;infer后跟的是将要推断的泛型。
检验成果,我们来实现一个GetReturnType
类型:利用infer实现内置泛型ReternType
,它的作用是从函数类型中提取返回类型(这就是我们将要推断的泛型)。
type GetReturnType<T> = T extends (...args: never[]) => infer R ? R : never;
type Str = GetReturnType<(x: string) => string>; //Str:string
type Bools = GetReturnType<(a: boolean, b: boolean) => boolean[]>; //Bools:boolean[]
复制代码
引入R
表示要推断的泛型,在前面加上infer,如果输入的泛型T
确实约束于函数类型(...args: never[]) => infer R
则返回类型R
,否则返回never
。
分布式条件类型
这个名字看起来有点高大上,其实就是在写条件类型的时候需要注意的一种情况,也可以称之为条件类型的特性吧。
当条件类型作用于泛型类型时,当传入泛型的是联合类型(union)时,这个时候条件类型会成为分布式类型。
根据上面的话,写一个例子:
// 下面类型的作用是将传入的泛型Type转化为数组类型
type ToArray<Type> = Type extends any ? Type[] : never;
// 传一个联合类型
type StrArrOrNumArr = ToArray<string | number>; //此时StrArrOrNumArr:string[] | number[]
复制代码
传入Type的是联合类型string | number
,然后分配为ToArray<string> | ToArray<number>
,最终得出string[] | number[]
的结果。这就是分布式条件类型。
如果不知道条件类型会分配的特性的话,可能会以为结果是(string | number)[]
。那真要得到这个结果,需要做一些简单修改:
type ToArrayNonDist<Type> = [Type] extends [any] ? Type[] : never;
复制代码
对Type
进行包裹之后,就不会产生分布式的情况了。
5.映射类型
映射类型建立在索引代号(index signature)的语法之上,索引代号用于声明未提前声明的属性类型。
所以在将映射类型之前,先讲索引代号。
当你想要申明一个类型或者接口,你只知道键和值类型,可以这样使用索引代号来代表:
type OnlyBoolsAndHorses = {
[key: string]: boolean | Horse;
};
// 等价于
type OnlyBoolsAndHorses1 = Record<string, boolean | Horse>;
const conforms: OnlyBoolsAndHorses = {
del: true,
rodney: false,
};
复制代码
类型OnlyBoolsAndHorses
表示未知键名和数量,只知道键名的类型是string
,键值的类型是boolean
或者Horse
。这种写法就叫作索引代号。此时也可以用TS内置的功能类型Record<Keys, Type>
来表示,都是一个意思。
下面来说映射类型。假设现在需要写一个接口B,它和接口A有一样的属性,那么我们就需要使用映射类型将A的属性类型映射到B接口。当然,你也可以笨办法,手动再copy一份修改,但是这始终不是好的解决办法。
看下面的例子:
type OptionsFlags<Type> = {
[Property in keyof Type]: boolean;
};
复制代码
在上面的例子中,OptionsFlags
将获取 Type 类型中的所有属性,并将其值更改为布尔值。keyof Type
前面讲过了是取Type里所有的属性,Property in
可以和JS里的for...in...
类比,依次取所有Type属性的每一个,并将它约束为boolean
类型。因为我们并不知道OptionsFlags
未来的属性有什么,只能通过泛型Type推断出约束属性的定义,所以使用索引代号的方式[...]:...
来表达OptionsFlags
类型最终的定义。
可以看出TS里的每个语法都非常简洁,底层已经帮我们实现了大部分功能,不像JS语言那么繁琐(比如循环一定是有for或者while关键字),但是这样封装带来的坏处就是比较晦涩、不能一眼看懂。这和写JS还是有很大区别的,我们需要不停的练习去习惯这样的写法。
继续,我们来完成本节开头所说的将A的属性类型映射到B接口:
interface A {
"name": string;
"age": number;
}
type mappingAToB<T> = {
[K in keyof T]: T[K];
};
let B:mappingAToB<A>; // 此时B的定义和接口A一致
复制代码
根据上面的类型,此时我们可以写出TS的内置工具类型Partial
,它的功能是将所有属性变为可选:
type Partial<T> = {
[K in keyof T]?: T[k];
};
复制代码
将可选符号?
替换为readonly
,就将所有属性变为只读了,又得到一个内置类型Readonly
:
type Readonly<T> = {
readonly [K in keyof T]: T[k];
};
复制代码
称热打铁,-
符号可以用在?
和readonly
前面,表示去除后面的属性。搭配一下-?
就会得到另外一个内置类型:Required
。 -?
表示去除可选,那所有属性都变成必须了,这不就是Required!
type Required<T> = {
[K in keyof T]-?: T[k];
};
复制代码
兄弟集美萌,看到这里是不是觉得超级简单!!!没有问题的,你的TS已经向高级慢慢迈进了。
6.类型守卫Type Guards
前面将条件类型的时候,埋下了类型守卫的坑。这个词乍一听也挺高大上并且摸不着头脑,主要原因是因为对type guards做直译导致的,其实我自己理解之后觉得翻译为类型保护可能更好吧。其实类型守卫的意思就是在TS里利用一些手段将大范围的类型收窄为一个小范围的类型,这些手段在TS里会保证程序不会出错,它们就叫做类型守卫。
一些TS的关键字,比如is、typeof、in、instanceof
都可以做类型守卫。简单来说typeof,类似JS里通过判断参数的类型来决定是否继续执行后面的代码,比如typeof a === 'string'
之后你才会继续用a.length
。
而在TS里,typeof
是妥妥的类型守卫,因为它修正了JS里的一个错误:
function printAll(strs: string | string[] | null) {
if (typeof strs === "object") { //null也是object
for (const s of strs) { //报错:Object is possibly 'null'.
console.log(s);
}
}
}
复制代码
在JS里的null也是object,如果不用TS写的话,这里的代码后续是非常容易出问题的。所以在 TypeScript 中,根据 typeof 返回的值进行检查是一种类型保护(守卫)。
再举个简单的例子:
对于union(联合)类型,有时候会通过定义type来分开联合类型,其实也就是将联合类型收窄为单个类型
type Transportation = { type:'car'; car:number } | {type:'bus'; bus:number}
// 收窄
function tran(arg:Transportation) {
// 类型守卫
if(arg.type === 'car'){
//此时类似收窄为{ type:'car'; car:number }
} else { ... }
}
复制代码
这里的arg.type
也是一种类型保护的行为。
is关键字
接下来讲is来做类型保护。
type a: number | string;
复制代码
如果你想将a类型收窄为string
类型,此时沿用JS的逻辑,会使用typeof
进行if else。我们先试试这样写:
//定义一个isString函数类型去判断是否是string类型,是就返回true
const isString = (arg: unknown): boolean => typeof arg === "string";
// 使用
function useIt(numOrStr: number | string) {
if (isString(numOrStr)) {
console.log(numOrStr.length); // 报错:number|string 上不含有length属性
}
}
复制代码
typeof arg === "string"
是一种类型守卫,但是上面可以看出联合类型并没有因为isString
改变,此时使用is
去强行做判断,就可以达到收窄的效果:
const isString = (arg: unknown): arg is string => typeof arg === "string";
复制代码
如果typeof arg === "string"
就用is
断定arg一定是string
类型,此时isString
函数类型就获得了和typeof arg === "string"
一样的守卫能力,这个时候useIt
函数就不会报错了。这种再封装方式也称作自定义类型守卫。
类型守卫在实际中可以用在检验外部未知数据,比如后端传到前端的数据。由于TS是在运行前进行预编译校验的,无法保证运行时的未知数据,此时就需要写一个自定义的类型守卫去保证程序正常的运行。
in 关键字
in关键字在之前讲keyof
时已经用到过。在JS里也有in
操作符,用来确定一个对象里是否含有某个属性:
var myObject = {
foo: 1,
};
'foo' in myObject; // true
复制代码
根据上述这一点,TS将其作为一种收窄潜在类型的方法。
举个例子:
type Fish = { swim: () => void };
type Bird = { fly: () => void };
function move(animal: Fish | Bird) {
if ("swim" in animal) { // 利用in去排除掉联合类型上其他的类型
return animal.swim();
}
return animal.fly();
}
复制代码
需要注意一点就是可选属性会通过in
的限制,这是in
守卫不了的。
type Fish = { swim: () => void };
type Bird = { fly: () => void };
type Human = { swim?: () => void; fly?: () => void };
function move(animal: Fish | Bird | Human) {
if ("swim" in animal) {
animal; // (parameter) animal: Fish | Human
} else {
animal; // (parameter) animal: Bird | Human
}
}
复制代码
7.收窄类型的多种方式
TS的handbook专门有一章来讲在不同情况下怎么去收窄类型,里面有11个小节。上面讲类型守卫的时候讲到了其中的几种:typeof、in、is和可分开的联合类型,接下来讲剩余部分,里面的思想大部分和JS一模一样。
使用相等操作符
使用==,!=,===,!==去收窄类型:
1.使用全等===
function example(x: string | number, y: string | boolean) {
if (x === y) {
x.toUpperCase(); // x:string
y.toLowerCase(); // y:string
} else {
console.log(x);
console.log(y);
}
}
复制代码
2.使用==:
==
符号在JS里属于不严格相等,在TS里也可以用来收窄类型。它对去除null
和undefined
有一个很好的效果,使用!=null
的时候,不仅仅把null剔除类型,也会把undefined也剔除出去。对于undefined同样也是,!=undefined
同样也剔除null
。
interface Container {
value: number | null | undefined;
}
function multiplyValue(container: Container, factor: number) {
// 'null' and 'undefined'同时被移除.
if (container.value != null) {
console.log(container.value); //(property) Container.value: number
// 可以安全计算'container.value'了
container.value *= factor;
}
}
复制代码
instanceof
这个和JS一模一样,不多说。
function logValue(x: Date | string) {
if (x instanceof Date) {
console.log(x.toUTCString()); // (parameter) x: Date
} else {
console.log(x.toUpperCase());// (parameter) x: string
}
}
复制代码
赋值
let x:number | string;
x = 1;
let a = x; // a:number
x = 'hello';
let b = x; // b:string
x = true; //Type 'boolean' is not assignable to type 'string | number'
let c = x; //c:number|string
复制代码
赋值给一个类型范围比较广的变量时,TS会根据右侧的值适当的收窄左侧变量的类型范围。
但是要注意的是,每一次赋值TS都是根据最初声明(declare) 的类型来做收窄的,并不会因为赋值改变最初声明的类型,所以上面例子中c
的类型为number|string
。
never
never 类型可以赋给每个类型,但是没有任何类型可以赋给 never (除了它本身)。利用这个特性可以对switch语句进行详尽的类型检查。
首先看JS的写法:
interface Circle {
kind: "circle";
radius: number;
}
interface Square {
kind: "square";
sideLength: number;
}
// ---cut---
type Shape = Circle | Square;
function getArea(shape: Shape) {
switch (shape.kind) {
case "circle":
return Math.PI * shape.radius ** 2;
case "square":
return shape.sideLength ** 2;
}
}
复制代码
一般在JS里,如果没有默认需要处理的,default
就不写,或者写的逻辑没办法兜底新增类型的情况。比如后来Shape
同事需要新增一个接口Triangle
,但是同事并不知道你之前写的switch用到了Shape
这个类型,此时应该去靠default
兜底。在TS里,就需要利用never
类型做兜底。
interface Circle {
kind: "circle";
radius: number;
}
interface Square {
kind: "square";
sideLength: number;
}
interface Triangle {
kind: "triangle";
sideLength: number;
}
// ---cut---
type Shape = Circle | Square | Triangle;
function getArea(shape: Shape) {
switch (shape.kind) {
case "circle":
return Math.PI * shape.radius ** 2;
case "square":
return shape.sideLength ** 2;
default:
const _exhaustiveCheck: never = shape; // Triangle无法赋值给never类型
return _exhaustiveCheck;
}
}
复制代码
在default
里加了这句 const _exhaustiveCheck: never = shape;
。shape
在第一次case的时候收窄为Square | Triangle
,第二次case的时候收窄为Triangle
,所以会有Triangle
无法赋值给never
类型的报错。
如果没有Triangle
类型,两次case导致类型收窄到没有类型,此时shape
的类型就变成never
了,又刚好never
可以赋值给never
类型,所以不会报错。
再复习一遍:never 类型可以赋给每个类型,但是没有任何类型可以赋给 never (除了它本身)。咋一看觉得never这个类型很多余,这个例子就有助于我们去理解never
这个类型的作用。
如果在项目中,对于某个大范围type,需要保证里面每一个类型都要被检查的话(也称作无穷尽检查),就需要结合never的特性像上面那样使用。
总结
其实TS不难,主要是很多专业名词比较陌生,但是只要能够和JS的概念对上就比较好理解了。TS的一些类型操作符和JS一样,也有一些虽然同名但是概念和用法不一致的。
我自己比较适应的方式就是刷类型体操的时候,先想想如果用JS的话应该怎么实现,然后再对应上TS去实现,这样前期对于入门的同学比较友好。