0%

TypeScript类型体操中的关键字详解(二)

使用

在各种类型操作中,少不了extends关键字的身影,它主要有以下几个作用: 接口继承 类型约束以及条件类型

接口继承

1
2
3
4
5
6
7
8
9
10
11
12
13
14
interface Person {
name: string;
age: number;
}

interface Player extends Person {
item: 'ball' | 'swing';
}
//接口继承后
// interface Person {
// name: string;
// age: number;
// item: 'ball' | 'swing';
// }

类型约束

通常和泛型一起使用,那么具体应该如何使用呢?

1
2
3
4
5
6
7
interface Dog {
bark: () => void
}

function dogBark<T extends Dog>(arg: T) {
arg.bark()
}

我们定义类型Dog,它 有一个不返回任何值的bark方法,使用extends关键字进行泛型约束,传入dogBark方法的值必须有bark方法,简单的说extends关键字在这里的作用:作为一个守门员,只让会狗叫的进,管你是不是🐕,只要会狗叫,就可以进;如果不会,请出门右转。

image.png

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
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关键字实现一些类型时,可能会用到如下代码:

1
P extends keyof T

表示P的类型是keyof T返回的字面量联合类型,也就是说P原本没限制,是any,限制之后类型变成了keyof T返回的字面量联合类型。

类似的有使用T extends keyof any对对象类型的属性进行约束,keyof any返回的是string | number | symbol,即这也是属性字段的取值范围。

再来看这样一个例子

1
2
3
4
5
6
interface Person {
name: string;
age: number;
}

type NameOf<T> = T['name'] // error: Type '"name"' cannot be used to index type 'T'

NameOf类型的作用是取得传入类型Tname属性的值的类型,但这里却报错了。因为传入的泛型T不一定有属性name, 传入的可能是一个没有name属性的对象,也可能是一个字面量类型,访问T可能没有的属性是不安全的,因此会报错,要解决这个问题就需要对泛型T进行约束,确保其一定具有name这个属性。

1
2
3
4
5
6
7
interface Person {
name: string;
age: number;
}

type NameOf<T extends {'name': unknown}> = T['name'] // success!
type personName = NameOf<Person> //string

条件类型 (Conditional Types )

常见表现形式为:

1
T extends U ? 'Y' : 'N'

可以这样理解:TU的子类型,那么返回结果是'Y', 否则是'N'. 类似JS中的三元表达式,其工作原理是类似的,例如:

1
2
3
4
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语句一样。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
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"



再来看如下代码:

1
2
3
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呢?实际发生的操作类似如下:

1
2
3
4
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关键字左侧的是一个泛型,且传入泛型的必须是联合类型,其他类型如交叉类型是没有分配效果的。

如果左侧不是泛型,直接传入一个联合类型,是没有分配效果的,只是一个简单的条件判断。

1
2
3
4
  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上一个高赞回答的例子

1
2
3
4
5
6
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类型约束特性相关的工具类型有PickRecord

Pick

Pick表示从一个类型中选取指定的几个字段组合成一个新的类型,用法如下:

1
2
3
4
5
6
7
8
9
type Person = {
name: string;
age: number;
address: string;
sex: number;
}

type PickResult = Pick<Person, 'name' | 'address'>
// { name: string; address: string; }

实现方式

1
2
3
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组合成了一个新的类型,用法如下:

1
2
3
4
5
6
7
8
9
10
11
12
type keys = 'Cat'|'Dot'
type Animal = {
name: string;
age: number;
}

type RecordResult = Record<keys, Animal>
// result:
// type RecordResult = {
// Cat: Animal;
// Dot: Animal;
// }

实现方式

1
2
3
type Record<K extends keyof any, T> = {
[P in K]: T
}

keyof any是什么鬼?鼠标放上去看看就知道了

因此,keyof anystring | number | symbol,先对键的取值范围进行了限定,只能是这三者中的一个。

Exclude & Extract & NonNullable

extends条件类型特性相关的工具类型又有哪些呢?

先看着两个:ExcludeExtract

Exclude<T, U>: 排除T中属于U的部分

image.png
Extract<T, U>: 提取T中属于U的部分,即二者交集

image.png

使用方法

1
2
type ExcludeResult = Exclude<'name'|'age'|'sex', 'sex'|'address'>
//type ExcludeResult = "name" | "age"
1
2
type ExcludeResult = Extract<'name'|'age'|'sex', 'sex'|'address'>
//type ExcludeResult = "sex"

实现方式

1
2
3
type Exclude<T, U> = T extends U ? never : T

type extract<T, U> = T extends U ? T : never

实现思路不再赘述,见前文extends分配条件类型的原理

NonNullable工具类型可以从目标类型中排除nullundefined,和Exclude相比,它将U限定的更具体。

实现也很简单:

1
2
3
4
5
6
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中指定的字段,用法如下:

1
2
3
4
5
6
7
8
type Person = {
name?: string;
age: number;
address: string;
}

type OmitResult = Omit<Person, 'address'>
// 结果:{ name?: string; age: number; }

实现方式

1
type Omit<T, K extends keyof any> = Pick<T, Exclude<keyof T, K>>

首先,删除指定字段,字段类型限定在 string | symbol number中,然后用ExcludeT的属性所组成的字面量联合类型中移除指定字段,形成新的联合类型;最后利用Pick选取指定字段生成新的类型

AppendToObject

AppendToObject的作用是向指定对象中添加一个属性, 同时指定属性值的类型。如果该属性字段之前就存在,新增的字段会被忽略。

注:该类型并不是内置工具类型

使用方式

1
2
3
4
5
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>

实现方式

1
2
3
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将两个类型合并成一个类型,第二个类型的键会覆盖第一个类型的键。

使用方式

1
2
3
4
5
6
7
8
9
10
11
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}

实现方式

1
2
3
type Merge<F, S> = {
[P in keyof F | keyof S]: P extends keyof S ? S[P] : P extends keyof F ? F[P] : never
}

这里使用了两次extends, 写成下方这种形式可能更清楚一些:

1
2
3
4
5
6
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

参考

ts handbook

TypeScript 的 extends 条件类型

type challenges