TypeScript
的工具类型,也被称作类型体操。通过本文你就知道这些工具类型的原理,并可以自己写出一些工具类型。在学习工具类型之前,我们先学学工具类型所用到的基础知识,当基础知识掌握牢固后,看懂工具类型自然水到渠成。
基础知识
泛型
我们先看一个泛型的例子:
1 | function identity<T>(arg: T): T { |
这个例子很简单,identity
函数接受一个名称为 T
泛型(名称可以随便写),函数参数的类型也是泛型 T
,返回值也是 T
。在调用函数的时候,根据具体的使用场景来决定参数的类型,这就是泛型的作用。如果类型不匹配的话就会报错。
在泛型类型声明的时候泛型可以是多个,可以给默认值,有默认值的泛型参数是可选泛型参数,可选泛型参数需要放在泛型定义的后端。
1 | function f1 <T = boolean, U>() {} // 错误 可选泛型应该放在后端 |
泛型约束
泛型约束是在泛型的类型参数上定义一个约束条件,从而限制了泛型实际类型的最大范围,这个类型参数的约束条件就是泛型约束,语法采用了 extends
关键字,类似于类的继承。泛型约束是工具类型的核心。
1 | interface Points { |
约束条件有点特殊,它可以引用泛型列表中的其他类型,但是不能循环引用。
1 | function f1 <T extends U, U>() {} // OK 注意约束条件可以引用列表中的其他类型 哪怕前面的都可以引用后面的 |
泛型类型别名
泛型除了用在函数上还可以用在类和类型别名上。在工具类型中,泛型大量应用与类型别名上,看两个例子:
1 | type Nullable<T> = T | undefined | null; |
联合类型
联合类型就是类型用 |
操作符连起来的类型。联合类型赋值的时候是相联合类型的综合。子类型与父类型联合的结果是父类型,任何类型与never
联合是任何类型。对于类、接口等对象的联合,可赋的值是其中任意一个,但访问只能访问所有类型的共有属性和方法。。从感觉上来看非对象联合范围可能变大了,但是对象的联合范围反而变小了。之所以这么做,是因为联合类型的对象在任何特定时刻只能符合其中的一个类型,因此 TypeScript 需要一种方法来确保你访问的属性在所有可能的类型中都是存在的。当然如果你通过类型判断推断出是具体的某个类型则可以调用对应的非共有方法。
1 | // 基本类型的联合 |
交叉类型
交叉类型就是类型用 &
操作符连起来的类型。子类型与父类型交叉的结果是子类型。对于没有交集类型的交叉则是 never
。对象类型的交叉是属性的综合。**never
、null
和 undefined
与其他类型交叉的结果是 never
,空对象类型与其他非never
、null
和undefined
的类型交叉是其他类型**。
1 | type T1 = boolean & true; // true |
如果交叉的两个属性相同,那么他们的属性类型也是每个属性交叉的结果,看一个稍微复杂的例子:
1 | interface IA { |
分配率
交叉类似于数学中的乘法,联合类似于数据中的加法,所有交叉的优先级比联合优先级高,同时满足分配率。
1 | A & B | C & D = (A & B) | (C & D) |
索引类型查询
通过索引类型查询(使用 keyof
关键字)能够获取给定类型中的属性名类型。索引类型查询的结果是由字符串字面量类型构成的联合类型。
1 | interface T { |
在JS中对象的键只能是字符串、数字和Symbol,所以 keyof
的结果必定是联合类型 number | string | symbol
的子类型。keyof
通常返回的是键的名称的联合类型,但有些情况比较特殊:
- 属性中只有字符串索引签名(属性类型是
[props: string]: any
),返回number | string
的联合类型; - 属性中只有数值索引签名(属性类型是
[props: number]: any
),返回number
类型; - 属性中只包含
unique symbol
属性,返回的是对应的unique symbol
类型; - 有其他属性成员的,返回的是成员属性名的联合类型(这条重要);
any
返回的是number | string | symbol
联合类型(通常用来做键的约束);unknown
返回never
;- 原始类型返回对应对象上的属性或方法名;
- 联合类型返回公共属性名(这条重要);
- 交叉类型返回所有属性名(这条重要)。
1 | interface I1 { |
索引访问类型
对象值的类型可以想对象一样获取:
1 | type T1 = {x: '1', [props: string]: string}; |
映射对象类型
映射对象类型可以把已有对象类型映射为新的对象类型,映射对应类型使用 in
关键字,语法定义如下,其中 readonly
和 ?
是可选的。
1 | { readonly [P in K] ?: T } |
看看例子:
1 | type T1 = { [P in 0 | 1] ?: string }; |
Partial 与 Readonly
本文讲的是TS的工具类型,到目前为止我们还没有讲工具类型,现在我们看看第一个工具类型 Partial<T>
。Partial<T>
的作用是把对象类型的所有属性设置为可选的,可以使用映射对象类型来实现:
1 | // 使用: |
同样的 Readonly<T>
也跟 Partial<T>
的实现方式类似,Readonly<T>
是给每一个属性加一个 readonly
:
1 | // 使用: |
同态映射对象类型
我们对象类型中存在索引类型查询(即 keyof
),这种映射就是同态映射,如果对象类型中没有索引类型查询就认为是非同态映射。同态映射会把原来对象的 readonle
和 ?
一并映射过来,所以是同态
。
1 | type T = {a?: string; readonly b: number}; |
注:非同态映射对于可选参数来说,类型是与
undefined
的联合类型。
同态映射对象类型只能保证映射的对象跟原有 readonle
和 ?
对象操作符相同,当然可以自己写一个 readonle
或 ?
覆盖调原来的操作符,但是不能随意控制减少。实际上在 readonle
和 ?
前可以添加 +
和 -
来更精细化的控制。当然 +readonle
和 readonle
的效果实际上是一样的,所以 +
一般是省略的。
现在我们实现一下 Required<T>
, 该工具类的作用就是去掉键值对中间的 ?
。
1 | // 使用: |
注:?
前加 -
的时候,只对带有 ?
的属性生效,且去掉 undefined
类型。对于没有 ?
修饰的属性,不去掉undefined
类型。
映射也可以用在非对象类型:
1 | type HMOT<T, X> = { [P in keyof T]: X}; |
条件类型
条件类型类似于JS中的三目运算符,只是类型的判断没有JS那么丰富。TS的类型检验是在编译阶段进行的,所以判断条件只有 extends
一个语句。条件判断示例如下:
1 | type T1 = true extends boolean ? string : number; // string |
条件表达式extends
前面的类型经可能使用裸类型
,所谓裸类型
就是没有任何装饰的类型参数。如下:
1 | type T1<T> = T extends string ? true : false; // T没有任何装饰是裸类型 |
对于裸类型可以展开:
1 | T = A | B |
上例中 T1T2
是裸类型可以展开相当于 (string extends string ? true : false) | (number extends string ? true : false)
也就是 true | false
也就是 boolean
, 但 T2T2
相当于 [string | number] extends [string] ? true : false
, 但是元组 [string | number]
不是元组 [string]
的子类型,所以是 false
。通常裸类型可以展开更符合类型判断。
类型查询
在JS中 typeof
可以判断一个变量的类型,TS对 typeof
做了扩展,在类型别名 type
等号右侧的 typeof
获取的是变量在TS中定义的类型。
1 | const a: string = typeof '123'; // 'string' |
infer关键字
我们看看本节最后一个知识点 infer
,这个看完你就会写工具类型了,是不是很开心?infer
功能非常强大,它可以反查类型,需要与 extends
配合使用。直接看例子:
1 | type ArrayItemType<T> = T extends Array<infer U> ? U : never; |
手撕工具类型
上面我们已经简绍了 Partial<T>
、Readonly<T>
和 Required<T>
,你应该已经明白一件事,要判断类型几乎一定会用到 extends
、in
或 keyof
,本节将手撕更多的工具类型。
TypeName
TypeName<T>
根据传入的T,返回T的类型的字符串,实际上这里的字符串是一种字符串字面量类型。
1 | // 使用: |
Exclude
Exclude<T, U>
可以在类型 T
中排除类型 U
。这里需要回顾一下在联合类型中,任何类型与 never
类型的联合是任何类型。
1 | // 使用: |
Extract
Exclude<T, U>
从 T
中提取 U
,通常 T
和 U
都是联合类型。
1 | // 使用: |
NonNullable
NonNullable<T>
如果 T
中包含了 null
或者 undefined
则删除 null
和 undefined
。
1 | // 使用: |
Record
Record<K, T>
把 K
中的值作为键,T
作为值的类型来构建对象。
1 | // 使用: |
Pick
Pick<T, K>
的作用是从对象类型 T
中,挑选出键在 K
中的属性,从而组成新的对象。
1 | // 使用: |
Omit
Omit<T, K>
与 Pick<T, K>
是互补的。它是已经类型删掉某些属性从而形成新的类型。
1 | // 使用: |
Parameters
Parameters<T>
,根据函数 T
的类型,返回函数 T
参数的元组。
1 | // 使用: |
ReturnType
ReturnType<T>
可以获取函数T的返回值类型。
1 | // 使用: |
ConstructorParameters
ConstructorParameters<T>
获取构造函数T的参数,返回对应参数的元组。
1 | // 使用: |
InstanceType
InstanceType<T>
获取构造函数返回值类型。
1 | // 使用: |
ThisParameterType
ThisParameterType<T>
获取函数的 this
参数类型,若没有定义 this
参数则返回 unknown
。
1 | // 使用: |
OmitThisParameter
OmitThisParameter<T>
能从函数 T
中剔除 this
函数类型。
1 | // 使用: |
我的博客即将同步至腾讯云开发者社区,邀请大家一同入驻:https://cloud.tencent.com/developer/support-plan?invite_code=1sfdnjsw8b22