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