TypeScript类型体操(一)
所谓类型体操,就是仅基于TypeScript的各种类型操作来实现一些工具类型,使用TypeScript的类型推导能力来运行具体的逻辑。借助泛型,模板字符串,推断等特性,可以绕开一些限制,构建出非常复杂的类型。知名开发者antfu在GitHub上发起了一个类型体操挑战项目type-challenges,里面收录了一系列由易到难的类型体操挑战,通过完成这些挑战可以极大地提升对TypeScript的理解,让我们在实际开发中能构建出更健壮的类型系统。
压缩体操基础
条件类型
TypeScript中的条件类型看起来有点像三元表达式:condition ? trueExpression : falseExpression
:
1 | SomeType extends OtherType ? TrueType : FalseType; |
当 extends
左边的类型可以赋值给右边的类型时,就会得到第一个分支(“true” 分支)的类型;否则,会获得“false”分支。
比如:
1 | type Flatten<T> = T extends any[] ? T[number] : T; |
Flatten
类型可以将一个数组类型展平为它们的元素类型。
条件类型提供了一种infer
关键字,可以让我们在条件语句中进行类型推断的方法,例如,我们可以推断出Flatten
中的元素类型,而不是使用索引访问类型来获取:
1 | type Flatten<T> = T extends Array<infer Item> ? Item : Type; |
这里使用了infer
引入了一个名为Item
的类型变量,而不是指定如何在true分支中通过索引来访问类型。
keyof
keyof
运算符生成一个对象类型的键的字符串或数字字面联合类型:
1 | type Point = { x: number; y: number }; |
typeof
JavaScript已经有一个typeof运算符了,TypeScript也有一个编译时的typeof,可以在类型的上下文中获取一个变量或属性的类型:
1 | let s = "hello"; |
这在这种普通用法上不是很明显,用在复杂的类型时是十分有用的,比如我们第一次尝试使用ReturnType时,可能是这种用的:
1 | function f() { |
这里会报错,是因为值和类型不是同一回事,我们要引用f的类型,因此用typeof:
1 | function f() { |
模板字符串类型
TypeScript中的模板字符串类型和JavaScript中的模板字符串有相似的语法:
1 | type Person = "Wenwazi"; |
当在插值位置使用联合类型时,得到的类型是由每个联合成员表示的每个可能得字符串的集合:
1 | type DB = { |
索引访问类型
我们可以使用索引访问来查找具体一个类型的属性:
1 | type Person = { age: number; name: string }; |
我们还可以使用任意类型的索引,比如number用来获取数组元素的类型,可以将它与typeof结合起来:
1 | const Persons = [ |
TS自带类型解析
TypeScript中自带了很多工具类型,这些工具类型算是类型体操的鼻祖。
Partial
Partial可以让类型里的属性变成可选的。
通过keyof
和in
,在每一个属性后加上?
,使属性变成可选的:
1 | type MyPartial<T> = { |
Required
Required可以让类型里的属性都变成必填的,和Partial相反。
和Partial一样,只需为每一个属性移除?修饰符(-?
),就可以让属性变成必填的:
1 | type Required<T> = { |
Readonly
Readonly可以让类型每一个属性变得只读。
和前面的一样,只需为每一个属性添加上readonly
标识符即可
1 | type Readonly<T> = { |
Pick<T,K>
Pick可以中T挑选出K中的属性,返回新的类型,因此K必须是T的键名联合:
1 | type Pick<T, K extends keyof T> = { |
Record<K,T>
Record可以创建类型K的键和类型T的值的对象,因此K应该是任意的类型。
1 | type Record<K extends keyof any, T> = { |
Exclude<T,U>
Exclude可以从联合类型T中,排除其中的类型U。这里要用到extends
,判断 T是否可以赋值给U,如果可以就返回never
,否则返回T:
1 | type Exclude<T, U> = T extends U ? never : T; |
Extract<T,U>
和Exclude相反,Extract从联合类型中,选出其中的类型U。
1 | type Extract<T, U> = T extends U ? T : never; |
Omit<T,K>
Omit和Pick相反,从T中省略掉K中的属性,返回一个新的类型,这里可以使用Exclude,和Pick结合,先从typeof T
中Exclude掉K中的属性,然后再Pick返回的属性。
1 | type Omit<T, K extends keyof T> = Pick<T, Exclude<keyof T, K>>; |
Parameters
Parameters用于获取一个函数参数的类型,因此,这里接受的T必须是一个函数,我们只需要拿到这个函数的参数类型即可,这里需要用到infer
,对参数的类型进行推导
1 | type MyParameters<T extends (...args: any[]) => any> = T extends (...args: infer U) => any |
Awaited
Awaited类型接收一个类Promise的类型,并且返回这个Promise的结果的类型。首先判断入参的类型是不是类Promise,这里可以用PromiseLike来判断,判断时推导出Promise的结果的类型,由于Promise是可以链式调用的,返回的结果也可以是Promise,所以要再判断一下,如果结果的类型是Promise的话,还需要递归一次,否则直接返回就好了。
1 | type MyAwaited<T extends PromiseLike<any>> = T extends PromiseLike<infer P> |
Easy
现在开始来实现type-challenges中的Easy级别的题目,这里不包含那些自带工具类型的实现。
First of Array
获取数组的第一个元素,可以先判断数组是不是空数组,如果是就返回never
,不是就返回第一个元素就好了。
1 | type First<T extends any[]> = T extends [] ? never : T[0]; |

Length of Tuple
获取数组的长度,这里需要注意的是,需要对类型使用readonly
,使用readonly
修饰符可以确保数组的长度是不变的,只有长度不变的数组在编译时才能获取到具体的长度,否则获取到的是number
。
1 | type Length<T extends readonly any[]> = T['length']; |

If
接收三个参数,实现类似三元表达式的效果,使用extends
判断第一个参数是不是为true
即可。
1 | type If<C extends boolean, T, F> = C extends true ? T : F; |

Concat
实现Concat,拼接两个数组,使用解构运算符即可。
1 | type Concat<T extends readonly any[], U extends readonly any[]> = [...T, ...U]; |

Include
实现Array.include
方法,需要用到递归的方法,从数组中推导出第一个元素和剩下元素的数组,判断第一个元素与包含的元素相等,是就返回true
,否则就递归判断剩下的元素的数组。
1 | type Includes<T extends any[], U> = T extends [infer FIRST, ...infer REST] |

Push
Array.Push
方法,使用解构运算符即可:
1 | type Push<T extends any[], U> = [...T, U]; |
