使用
在各种类型操作中,少不了extends关键字的身影,它主要有以下几个作用: 接口继承 类型约束以及条件类型
接口继承
1 | interface Person { |
类型约束
通常和泛型一起使用,那么具体应该如何使用呢?
1 | interface Dog { |
我们定义类型Dog,它 有一个不返回任何值的bark方法,使用extends关键字进行泛型约束,传入dogBark方法的值必须有bark方法,简单的说extends关键字在这里的作用:作为一个守门员,只让会狗叫的进,管你是不是🐕,只要会狗叫,就可以进;如果不会,请出门右转。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17let dogA = {
weight: 12,
age: 4
}
let dogB ={
weight: 12,
age: 4,
bark: () => console.log('dogB is barking')
}
dogBark(dogA)
// error !!!
// Argument of type '{ weight: number; age: number; }' is not assignable to parameter of type 'Dog'.
// Property 'bark' is missing in type '{ weight: number; age: number; }' but required in type 'Dog'.
dogBark(dogB) // success: "dogB is barking"
在使用extends关键字实现一些类型时,可能会用到如下代码:
1 | P extends keyof T |
表示P的类型是keyof T返回的字面量联合类型,也就是说P原本没限制,是any,限制之后类型变成了keyof T返回的字面量联合类型。
类似的有使用T extends keyof any对对象类型的属性进行约束,keyof any返回的是string | number | symbol,即这也是属性字段的取值范围。
再来看这样一个例子
1 | interface Person { |
NameOf类型的作用是取得传入类型T中name属性的值的类型,但这里却报错了。因为传入的泛型T不一定有属性name, 传入的可能是一个没有name属性的对象,也可能是一个字面量类型,访问T可能没有的属性是不安全的,因此会报错,要解决这个问题就需要对泛型T进行约束,确保其一定具有name这个属性。
1 | interface Person { |
条件类型 (Conditional Types )
常见表现形式为:
1 | T extends U ? 'Y' : 'N' |
可以这样理解:T是U的子类型,那么返回结果是'Y', 否则是'N'. 类似JS中的三元表达式,其工作原理是类似的,例如:
1 | type res1 = true extends boolean ? true : false // true |
要注意:
-
extends在条件类型中的作用和类型约束中的作用不一样 - 条件类型只支持在
type中使用
此外,**extends作为条件类型时也是可以嵌套的**,就像if语句一样。
1
2
3
4
5
6
7
8
9
10
11
12
13
14type TypeName<T> =
T extends string ? "string" :
T extends number ? "number" :
T extends boolean ? "boolean" :
T extends undefined ? "undefined" :
T extends Function ? "function" :
"object";
type T0 = TypeName<string>; // "string"
type T1 = TypeName<"a">; // "string"
type T2 = TypeName<true>; // "boolean"
type T3 = TypeName<() => void>; // "function"
type T4 = TypeName<string[]>; // "object"
再来看如下代码:
1 | type A1 = P<'x' | 'y'> extends 'x' ? string : number; // type A1 = number |
A2结果为什么不是number呢?实际发生的操作类似如下:
1 | type A2 = P<'x' | 'y'> |
这叫分配条件类型(Distributive Conditional Types)
当T为泛型时,且传入该泛型的是一个联合类型,那么该联合类型中的每一个类型都要进行上述操作,最终返回上述操作结果组成的新联合类型。换句话说,这里的分配是指将上述提到的”三元表达式”操作应用于联合类型中的每个成员。
要注意的是:
1. extends关键字左侧的是一个泛型,且传入泛型的必须是联合类型,其他类型如交叉类型是没有分配效果的。
如果左侧不是泛型,直接传入一个联合类型,是没有分配效果的,只是一个简单的条件判断。
1 | type A1 = 'x' extends 'x' ? string : number; // string |
2. 分配操作只有在检查的类型是naked type parameter时才生效。
那么是什么是naked type parameter呢?直接翻译过来怪怪的,参数是裸的?
我的理解是没有对传进来的泛型参数进行一些额外操作,那么就符合naked type parameter的要求。
看一下以下的例子,更容易理解。这也是stackoverflow上一个高赞回答的例子:
1 | type NakedUsage<T> = T extends boolean ? "YES" : "NO" |
其中,WrappedUsage对传入的泛型参数进行了操作,不属于naked type parameter,因此不会进行分配操作。
类型操作实战
Pick & Record
与extends类型约束特性相关的工具类型有Pick和Record
Pick
Pick表示从一个类型中选取指定的几个字段组合成一个新的类型,用法如下:
1 | type Person = { |
实现方式
1 | type Pick<T, K extends keyof T> = { |
首先进行了类型限定,K一定是T的子集,然后用in遍历K中的每个属性, T[P]是属性对应的值。
Record
Record<K, T>用来将K的每一个键(k)指定为T类型,这样由多个k/T组合成了一个新的类型,用法如下:
1 | type keys = 'Cat'|'Dot' |
实现方式
1 | type Record<K extends keyof any, T> = { |
keyof any是什么鬼?鼠标放上去看看就知道了
因此,keyof any即string | number | symbol,先对键的取值范围进行了限定,只能是这三者中的一个。
Exclude & Extract & NonNullable
与extends条件类型特性相关的工具类型又有哪些呢?
先看着两个:Exclude和Extract
Exclude<T, U>: 排除T中属于U的部分
Extract<T, U>: 提取T中属于U的部分,即二者交集
使用方法
1 | type ExcludeResult = Exclude<'name'|'age'|'sex', 'sex'|'address'> |
1 | type ExcludeResult = Extract<'name'|'age'|'sex', 'sex'|'address'> |
实现方式
1 | type Exclude<T, U> = T extends U ? never : T |
实现思路不再赘述,见前文extends分配条件类型的原理
NonNullable工具类型可以从目标类型中排除null和undefined,和Exclude相比,它将U限定的更具体。
实现也很简单:
1 | type A = null | undefined | 'dog' | Function |
Omit
根据已经实现的Exclude类型,可以实现Omit类型,Omit<T, K>:删除T中指定的字段,用法如下:
1 | type Person = { |
实现方式
1 | type Omit<T, K extends keyof any> = Pick<T, Exclude<keyof T, K>> |
首先,删除指定字段,字段类型限定在 string | symbol number中,然后用Exclude从T的属性所组成的字面量联合类型中移除指定字段,形成新的联合类型;最后利用Pick选取指定字段生成新的类型
AppendToObject
AppendToObject的作用是向指定对象中添加一个属性, 同时指定属性值的类型。如果该属性字段之前就存在,新增的字段会被忽略。
注:该类型并不是内置工具类型
使用方式
1 | type Test = { id: '1' } |
实现方式
1 | type AppendToObject<T, K extends keyof any, V> = { |
首先, 需要遍历的所有属性包含T中的属性字段和新增的字段K,即keyof T | K,然后使用in关键字进行遍历操作,对遍历到的每个属性字段使用extends进行判断,如果遍历到的字段P是原本T中就存在的属性字段,判断为true,返回T[p];否则为false,说明该属性字段之前并不存在,返回新增字段对应的类型V。
Merge
Merge将两个类型合并成一个类型,第二个类型的键会覆盖第一个类型的键。
使用方式
1 | type foo = { |
实现方式
1 | type Merge<F, S> = { |
这里使用了两次extends, 写成下方这种形式可能更清楚一些:
1 | type Merge<F, S> = { |
和AppendToObject一样,首先用in关键字遍历所有的属性字段(keyof F | keyof S), 在此过程中对每一字段进行相应判断,因为S中的对应的字段会对F中相同字段进行覆盖,因此先判断该字段是否属于S,然后再判断该字段是否属于P。
参考