用了 TypeScript 一段时间后,自我感觉对 TypeScript 工具类型有一定理解了,随手就能噼里啪啦出一大堆 Partial
、Omit
啥的。但就是吧,有时会遇到一些复杂的业务场景(或需要对第三方库的类型做进一步转换时),会突然的卡住。卡住?没错,就是那种突然不知代码该怎么往下写的感觉,然后陷入了自我怀疑…
这种感觉,可能也就伍佰老师能懂吧 … 😏
在这篇文章中,我们从官方文档着手并结合网上的一些文章以及其源码实现,重新理解并梳理一下 TypeScript 中「工具类型」这块内容。
一、什么是「工具类型」?
试想一个场景,你正在写一个 “统计社区居民信息” 的需求,业务要求所有的居民都必须填写 “姓名”、”性别” 和 “职业”,但 “年龄” 可选。于是,你这样设计了 “居民信息组件” 的属性类型:
interface ResidentProps {
name: string
gender: string
work: string
age?: number
}
临下班是,你发现得写个 “男性居民信息组件”,于是这样定义了它的属性类型:
interface GentlemenProps {
name: string
work: string
age?: number
}
第二天,你发现还有个 “(填写了年龄的)男性居民信息组件” 要写,于是又多了这样一个属性类型:
interface GentlemenWithAgeProps {
name: string
work: string
age: number
}
回头看了两眼好像不太对劲,这三个类型长得都差不多,这样反反复复定义来定义去也太傻 x 了吧?而且,当我觉得一开始定义的 work
字段用来表示 “职业” 不太准确,想改成 profession
时还得一个个去改。这不是好鸡肋吗?就没有一些工具能辅助我 “快速完成类型定义” 且 “规避掉上述提到的「一个个去改」的问题” 吗?
我太难了 … 😔
骚年淡定,这个可以有,你打开 官方文档(utility-types )部分 就会看到这样一句话:
TypeScript provides several utility types to facilitate common type transformations. These utilities are available globally.
通俗点说就是:TypeScript 提供了一系列的「工具类型」来使得 “常见的类型转换” 变得更加容易,且这些「工具类型」是全局可用的,不需要 import
就能直接用的。
照猫画虎(用了「工具类型」)后,发现上面的类型定义简化了好多:
interface ResidentProps {
name: string
gender: string
work: string
age?: number
}
// 仅省去 ResidentProps 中的 gender
type GentlemenProps = Omit<ResidentProps, 'gender'>
// 将 GentlemenProps 中所有的属性都变成必填的
type GentlemenWithAgeProps = Required<GentlemenProps>
而且,当我想把 work
属性改成 profession
时,真正要去做的也就是像下面这样调整下 ResidentProps
类型的代码就好了。
在「工具类型」作用下,
GentlemenProps
和GentlemenWithAgeProps
会自动跟着调整 “职业” 的类型声明。
interface ResidentProps {
name: string
gender: string
- work: string
+ gender: string
age?: number
}
实际上,TypeScript 提供的「工具类型」远不止上面提到的 Omit
和 Required
,下面我们将会一一解析。
二、 「工具类型」详解
1、Partial<T>
用于构造一个新的类型,它有着给定类型 T
的所有属性,且每个属性都被设置成了 “可选“。举个栗子:
interface Todo {
title: string
desc: string
}
type Todo2 = Partial<Todo>
// 上面的 Todo2 等效于下面的定义
type Todo2 = {
title?: string
desc?: string
}
2、Require<T>
用于构造一个新的类型,它有着给定类型 T
的所有属性,且每个属性都被设置成了 “必选“。举个栗子:
interface Todo {
title?: string
desc?: string
}
type Todo2 = Required<Todo>
// 上面的 Todo2 等效于下面的定义
type Todo2 = {
title: string
desc: string
}
可以看出,Partial
和 Required
其实是 “对立” 的一组「工具类型」,前者设属性 “可选“,后者设属性 “必选“ 。
3、Readonly<T>
用于构造一个新的类型,它有着给定类型 T
的所有属性,且每个属性都被设置成了 “只读“。举个栗子:
interface Todo {
title: string
desc: string
}
type Todo2 = Readonly<Todo>
// 上面的 Todo2 等效于下面的定义
type Todo2 = {
readonly title: string
readonly desc: string
}
这意味当你想要修改 Todo2
对象实例的任何属性时,会有语法报错:
const todo: Todo = { title: '吃苹果', desc: '今天中午要吃一个苹果' }
// ✅ 符合类型定义逻辑
todo.title = '吃苹果啦'
const todo2: Todo2 = { title: '吃苹果', desc: '今天中午要吃一个苹果' }
// ❌ 不符合类型定义逻辑:
// Cannot assign to 'title' because it is a read-only property.
todo2.title = '吃苹果啦'
所以在日常开发中,当你暴露的方法给外部调用但又不希望该方法返回结果被他人修改时,可以显式地给返回值声明为 “只读“:
function getSomethingFreezed<T>(obj: T): Readonly<T>
4、Record<K, T>
构造一个新的对象类型(object type):
- 它的「键」都是联合类型
K
的成员。 - 它的「值」的类型都是
T
。
通常被用来将一个类型的 properties
映射成另一个类型,举个栗子:
interface Todo {
title: string
desc: string
}
type Order = 'first' | 'second' | 'third'
type Todos = Record<Order, Todo>
// 上面的 Todos 等效于下面的定义
type Todos = {
first: Todo
second: Todo
third: Todo
}
5、Pick<T, K>
构造一个新的对象类型(object type):
- 它的属性集合是
T
的属性集合的子集。 - 具体节选哪些属性则由
K
来指定。K
可以是「单个字符串字面量(string literal)」,也可以是「联合字符串字面量(union of string literals)」。
举俩栗子:
// Example 1
interface Todo {
title: string
desc: string
completed: boolean
}
type Todo2 = Pick<Todo, 'title'>
// 上面的 Todo2 等效于下面的定义
interface Todo2 {
title: string
}
// Example 2
interface Todo {
title: string
desc: string
completed: boolean
}
type Todo2 = Pick<Todo, 'title' | 'completed'>
// 上面的 Todo2 等效于下面的定义
interface Todo2 {
title: string
completed: boolean
}
6、Omit<T, K>
构造一个新的对象类型(object type):
- 它的属性集合是
T
的属性集合 “忽略了部分属性后” 的子集。 - 具体忽略哪些属性则由
K
来指定。同
Pick
,其中K
可以是 「单个字符串字面量(string literal)」,也可以是「联合字符串字面量(union of string literals)」,下文有类似用法的话不再赘述。
还是俩栗子:
// Example 1
interface Todo {
title: string
desc: string
completed: boolean
}
type Todo2 = Omit<Todo, 'desc' | 'completed'>
// 上面的 Todo2 等效于下面的定义
interface Todo2 {
title: string
}
// Example 2
interface Todo {
title: string
desc: string
completed: boolean
}
type Todo2 = Omit<Todo, 'desc'>
// 上面的 Todo2 等效于下面的定义
interface Todo2 {
title: string
completed: boolean
}
可以看到,上面示例中的 Omit<Todo, 'desc'>
和前面示例中的 Pick<Todo, 'title' | 'completed'>
的结果是一致的。但啥时候用 Omit
啥时候用 Pick
,得看使用场景和看个人爱好了。
7、Exclude
构造一个新的类型:
- 它是由 “联合类型
T
的成员” 中排除 “在联合类型U
的相同成员” 后的剩余成员联合起来的一个新类型。
有点抽象?没关系,吃颗个栗子就能理解了:
// 「周一到周日」
type Days = 'Mon.' | 'Tues.' | 'Wed.' | 'Thurs.' | 'Fri.' | 'Sat.' | 'Sun.'
// 「周六、周日」
type Weekend = 'Sat.' | 'Sun.'
// 从 "Days 成员" 中移除在 "Weekend 中存在的成员" 后,剩余的成员就是「周一到周五」啦
type Workday = Exclude<Days, Weekend>
// 上面的 Workday 等效于下面的定义
type Workday = 'Mon.' | 'Tues.' | 'Wed.' | 'Thurs.' | 'Fri.'
稍微能理解了吧?那我来问两个问题:
- Q1:如果
U
比T
大,构造出来的类型是啥? - Q2:如果
U
中有成员不在T
中,构造出来的类型又是啥?
答案是:
- A1:构造出来的类型是
never
。 - A2:排除成员时,只会排除在
T
中的成员,不在T
中的成员会被忽略掉。never
类型:不存在的值的类型,具体可见 TypeScript 中的 never 类型具体有什么用?。
上面的两个答案用一句话概括也行:
- 排除时仅排除
U
在T
中的成员,所有成员都排除完了得到never
类型,否则得到 “剩余成员联合起来的类型” 。
举些栗子来看下:
// Example 1
type T1 = 'A' | 'B'
type T2 = 'A' | 'B'
type T3 = Exclude<T1, T2> // = never
// Example 2
type T1 = 'A' | 'B'
type T2 = 'A' | 'B' | 'C'
type T3 = Exclude<T1, T2> // = never
// Example 3
type T1 = 'A' | 'B'
type T2 = 'A' | 'C'
type T3 = Exclude<T1, T2> // = 'B'
// Example 4
type T1 = 'A' | 'B'
type T2 = 'C'
type T3 = Exclude<T1, T2> // = 'A' | 'B'
实际上,Exclude<T, U>
不仅可用于排除「字符串字面量联合类型」,也可用于排除「非字符串字面量联合类型」,再举个栗子:
// Example 5
type T1 = number | string | (() => void)
type T2 = () => void
type T3 = Exclude<T1, T2> // = number | string
从 T3 的结果来看,我们已经去掉了 T1
中的 () => void
,只留下了 number
和 string
。
8、Extract<T, U>
构造一个新的类型:
- 它是从 “联合类型
T
的成员” 中提取出 “在联合类型U
的相同成员” 后联合起来的一个新类型。前面提到的
Exclude<T, U>
与该「工具类型」要达到的目的其实一致的,只是两者去罗马的路不一样而已:前者 “排除“,后者 “提取“。
免闲谈,上栗子:
// Example 1
type T1 = 'A' | 'B' | 'C'
type T2 = 'A' | 'C'
type T3 = Extract<T1, T2> // = 'A' | 'C'
// Example 2
type T1 = 'A' | 'B' | 'C'
type T2 = 'A' | 'B' | 'C' | 'D'
type T3 = Extract<T1, T2> // = 'A' | 'B' | 'C'
// Example 3
type T1 = 'A' | 'B' | 'C'
type T2 = 'A' | 'D'
type T3 = Extract<T1, T2> // = 'A'
// Example 4
type T1 = 'A' | 'B' | 'C'
type T2 = 'D'
type T3 = Extract<T1, T2> // = never
// 一句话概括:
// 根据 `U` 来提取 `T` 中有的,没有的不提,提不到就 `never`
9、NonNullable<T>
构造一个 新的T
的非空类型:
- 它是把
T
中null
和undefind
成员都排除掉后得到的类型。
举俩栗子:
// Example 1
type T1 = string | number | null | undefined
type T2 = NonNullable<T1> // = string | number
// Example 2
type T1 = string[] | null | undefined
type T2 = NonNullable<T1> // = string[]
10、Parameters
构造一个新的类型:
- 它是从函数类型
T
的参数列表中依次获取参数类型后得到的一个元组类型。
举仨栗子:
// Example 1
type F = () => void
type T = Parameters<F> // = []
const t1: T = [] // ✅ Type correct
const t2: T = ['a'] // ❌ Type '[string]' is not assignable to type '[]'
// Example 2
type F = (a: string) => void
type T = Parameters<F> // = [a: string]
const t1: T = ['hello'] // ✅ Type correct
const t2: T = [123] // ❌ Type 'number' is not assignable to type 'string'
// Example 3
type F = (a: string, b: number) => void
type T = Parameters<F> // = [a: string, b: number]
const t1: T = ['hello', 123] // ✅ Type correct
const t2: T = ['hello', 'world'] // ❌ Type 'string' is not assignable to type 'number'
看起来挺厉害的就是不知道啥场景会用到,举一个?好勒:
// 假设第三方库提供了一个函数
export function repeatSomething(
target: string | null | undefined,
count: number)
{
return target ? target.repeat(count) : target
}
// 因为某种原因,你得在 repeatSomething 基础上封装一个函数给身边的小伙伴用
import { repeatSomething } from 'an-awesome-lib'
// 1️⃣ 抽离出 repeatSomething 函数参数的元组类型
type RepeatParams = Parameters<typeof repeatSomething>
// 2️⃣ 在 myFuncion 参数中直接使用该类型来标注参数
// 这样在调用 repeatSomething 时就可以通过「解构数组」来传参啦
export function myFuncion(myFlag: boolean, repeatParams: RepeatParams) {
const repeated = repeatSomething(...repeatParams)
if (myFlag) {
// do something with `repeated` ...
} else {
// do something else with `repeated` ...
}
}
11、ReturnType<T>
构造一个新的类型:
- 它等同于函数类型
T
的返回值的类型。
举些栗子:
// Example 1
type T1 = () => void
type T2 = ReturnType<T1> // = void
// Example 2
type T1 = (a: string) => void
type T2 = ReturnType<T1> // = void
// Example 3
type T1 = () => string
type T2 = ReturnType<T1> // = string
// Example 4
type Todo = {
title: string
desc: string
}
type T1 = () => Todo
type T2 = ReturnType<T1> // = Todo
// Example 5
type T1 = <T extends number[]>(a: T) => T
type T2 = ReturnType<T1> // = number[]
TypeScript 中还有一些关于
class
和this
的「工具类型」,比如 ConstructorParameters、 InstanceType 、ThisParameterType 、OmitThisParameter 和 ThisType 。 因为本人日常工作中很少会用,这里暂且跳过,感兴趣的朋友可以自己上去看下。
三、「工具类型」源码分析
所谓顾其名知其意,「工具类型」中的 “工具” 表示它实际上是 TypeScript 封装好的的一些工具而已,在形式跟我们日常开发中在 utils.ts
中定义的方法是差不多的。区别在于这些「工具类型」是全局可用、开箱即用的。
既然这样,如果理解了这些「工具类型」的源码实现,是不是有助于我们在工作中更加灵活的使用 TypeScript 的类型。
1、Partial<T>
/**
* Make all properties in T optional
*/
type Partial<T> = {
[P in keyof T]?: T[P];
};
我们知道,在声明和使用对象类型时,姿势大致是这样的:
// 该对象类型所有的 `key` 都是 string,所有的 `value` 都是 number
type Obj = {
[key: string]: number
}
// 声明一个 Obj 的实例
const obj: Obj = {
age: 18, // ✅ Type correct
name: 'zhangsan', // ❌ Type 'string' is not assignable to type 'number'
}
那么,上面源码中的 [P in keyof T]?: T[P]
是个啥意思呢?
以前面的 Todo
为例,然后拆解来看:
// 1️⃣ `keyof T` 作用是把 T 的 key 都拿出来,形成一个 "字符串字面量联合类型"
type Todo = {
title: string
desc: string
}
type T1 = keyof Todo
// 所以上面的 keyof 其实等价于下面
type T1 = "title" | "desc"
// 2️⃣ `P in U` 则限定了 P 必须是联合类型的成员之一,所以
type T1Obj = {
[P in T1]: string
}
const t1Obj: T1Obj = {
title: '吃苹果', // ✅ Type correct
desc: '今天中午吃一个苹果', // ✅ Type correct
completed: '1', // ❌ 'completed' does not exist in type 'T1Obj'
}
// 3️⃣ `T[P]` 作用是获取 "对象类型 T" 的 "键",举个栗子
type TodoPro = {
title: string
desc: string
completed: boolean
timestamp: number
}
type T2 = TodoPro['title'] // = string
type T3 = TodoPro['desc'] // = string
type T4 = TodoPro['completed'] // = boolean
type T5 = TodoPro['timestamp'] // = number
type T6 = TodoPro['wth'] // ❌ Property 'wth' does not exist on type 'TodoPro'
综合拆解后的分析来看,[P in keyof T]?: T[P]
其实就是:
- 构造的新的对象类型,其
key
都是T
的key
,不多不少。 - 构造的新的对象类型,其
value
的类型与T
中对应key
的类型相同。 key
都给加了个问号,表示这个构造的新的对象类型的key
都是可选的。
2、Require<T>
/**
* Make all properties in T required
*/
type Required<T> = {
[P in keyof T]-?: T[P];
};
看起来跟 Partial<T>
源码实现的差别就只是:
- “将
?
替换成了-?
“。
表示有点懵:
?
经常看到,可-?
是个啥玩意 …
谷歌一波后找到了个靠谱答案( What does -? mean in TypeScript ? ),大意如下:
- 我们可以通过
+
或-
来控制「映射类型修饰符」比如?
或readobly
。 - 其中
-?
意为着 “必须全部存在”,也就是说它帮你把 “可选性?
“ 都给删掉了。 - 其中
+?
意味着 “全部都是可选的”,也就是说它帮你给没有 “可选性?
“ 的key
加上了可选性。
代码演示如下:
type T = {
a: string
b?: string
}
// 此时 T1 和 T 相同:`a` 为 "必选",`b` 为 "可选"
const T1: { [P in keyof T]: string } = {
a: 'AA',
}
// 酱紫 `a` 变为 "可选" 了
const T2: { [P in keyof T]+?: string } = {
}
// 酱紫 `a` 变为 "可选" 了(`+?` 简写成 `?`)
const T3: { [P in keyof T]?: string } = {
}
// 酱紫 `b` 变为 "必选" 了
const T4: { [P in keyof T]-?: string } = {
a: 'AA',
b: 'BB',
}
3、Readonly<T>
/**
* Make all properties in T readonly
*/
type Readonly<T> = {
readonly [P in keyof T]: T[P];
};
这个也有点类似 Partial<T>
,只是将 key
的修饰符 ?
替换成了 readonly
。
4、Record<T>
Record<T>
源代码:
/**
* Construct a type with a set of properties K of type T
*/
type Record<K extends keyof any, T> = {
[P in K]: T;
};
有点意思,在泛型声明中多了个 extends
关键字,而且是 keyof
的是 any
,为啥要这样? 下面我们也是拆解后来看。
extends
先来看下通过 interface
关键字声明类型的一个姿势:
interface T1 {
x: number
y: number
}
interface T2 extends T1 {
z: number
}
// 此时 T2 等效于下面的定义
interface T2 extends T1 {
x: number
y: number
z: number
}
上面的 T2
通过 extends
关键字继承了 T1
所有的属性,那问题来了:
- 从 “表示范围” 来看,
T1
大还是T2
大?
答案是:
T1
的表示范围比T2
的大。
可是你会想到:
T2
不是比T1
多了z: number
么?
是的,就因为多了个 z: number
,所以 T2
表示的范围就小了。举个栗子:
- 现在广场上有 10 个人,其中 3 个拿着苹果和西瓜,3 个拿着西瓜和橘子,剩余 4 个拿着橘子和苹果。
- 你大喊一声:拿着苹果的走出来,此时共有 7 个人。
- 然后你在大喊一声:拿着苹果和西瓜的走出来,此时共有 3 个人。
- 紧接着又大喊一声:拿着苹果、西瓜和橘子的走出来,此时共有 0 个人。
所以说:
- 当你的限定条件越来越多时,你圈中的范围就会越来越小。
结合上面声明类型的示例代码,在进行「条件类型检查」时你会得到这样的结果:
interface T1 {
x: number
y: number
}
interface T2 extends T1 {
z: number
}
// 结果:
type IsT1ExtendsT2 = T1 extends T2 ? true : false // = false
type IsT2ExtendsT1 = T2 extends T1 ? true : false // = true
由此可以得出结论:
- 在对象类型中,多一个
key
之后表示的范围会比原来的小。
然后,再来看下在「联合类型」中进行「条件类型检查」的结果是怎样:
type T1 = 'first' | 'second' | 'third'
type T2 = 'first' | 'second'
// 结果:
type IsT1ExtendsT2 = T1 extends T2 ? true : false // = false
type IsT2ExtendsT1 = T2 extends T1 ? true : false // = true
从结果看,我们可以得出另外一个结论:
- 在联合类型中,多一个
成员
之后表示的范围会比原来的大。
keyof any
上面的 extends
部分我们能够理解了,接下来看 keyof any
。回想一下:
- JavaScript 对象中的
key
有哪几种类型?
答案是:
- 3 种,
string
、number
和symble
。
举个栗子:
const num = 123
const symble = Symbol('This is a Symble')
const obj = {
name: '哈哈',
[num]: '呵呵',
[symble]: '呃..'
}
console.log(obj.name) // '哈哈'
console.log(obj[num]) // '呵呵'
console.log(obj[symble]) // '呃..'
实际上,keyof any
就可以用来表示 “能够作为对象的 key
“ 的类型,即 string | number | symbol
,不信你看 😏 :
小结
综合上面的 extends
和 keyof any
的分析,可知只要传入的 Record<K extends keyof any, T>
中的 K
的范围小于联合类型 string | number | symbol
,那么类型检查就是正确的。举些栗子:
type Todo = {
title: string
desc: string
}
type T1 = Record<'first', Todo> // ✅ Type correct
type T2 = Record<'first'| 1234567, Todo> // ✅ Type correct
type T3 = Record<'first' | 1234567 | symbol, Todo> // ✅ Type correct
const myTodo = {
title: '吃块西瓜',
desc: '今天中午吃块 🍉 吧'
}
const myNum = 1234567
const mySymble = Symbol('This is my symble')
const t4: T4 = {
first: myTodo,
[myNum]: myTodo,
[mySymble]: myTodo
}
type T4 = Record<{ a: number }, Todo> // ❌ Type '{ a: number; }' does not satisfy the constraint 'string | number | symbol'
type T5 = Record<() => void, Todo> // ❌ Type '() => void' does not satisfy the constraint 'string | number | symbol'
5、Pick<T>
/**
* From T, pick a set of properties whose keys are in the union K
*/
type Pick<T, K extends keyof T> = {
[P in K]: T[P];
};
如果前面的 Record<K extends keyof any, T>
源码实现都看懂了,这个跳过就好,哈哈。
6、Omit<T>
/**
* Construct a type with the properties of T except for those in type K.
*/
type Omit<T, K extends keyof any> = Pick<T, Exclude<keyof T, K>>;
等号左右的 K extends keyof any
我们知道是:
- 将
K
的范围限定在string | number | symbol
之下。
等号右边的感觉有点绕,我们结合一个实例来看下:
interface Todo {
title: string
desc: string
completed: boolean
}
type Todo2 = Omit<Todo, 'desc'>
在该实例中我们可以看出:
keyof T
等效于'title' | 'desc' | 'completed'
。K
等效于desc
。
也就是说,Exclude<keyof T, K>
实际上:
- 等效于
Exclude<'title' | 'desc' | 'completed', 'desc'>
。 - 即等效于
'title' | 'completed'
。
这样的话,等号右边的 Pick<T, Exclude<keyof T, K>>
实际上:
- 等效于
Pick<Todo, 'title' | 'completed'>
综合来看可以得出结论:
Omit
实际上是通过Exclude
实现的一个 “反转Pick
“ 。
7、Exclude<T>
/**
* Exclude from T those types that are assignable to U
*/
type Exclude<T, U> = T extends U ? never : T;
可以看到,这里通过 extends
关键字进行了「条件类型检查」:
- 当
T
的范围小于U
时,可以理解成是把T
中所有的成员排除掉,没有成员后就 “没有能表示其类型的类型” 了,于是结果为never
。 - 当
T
的范围大于U
时,结果却还是T
,怎么理解?
回到我们之前的实例代码:
type Days = 'Mon.' | 'Tues.' | 'Wed.' | 'Thurs.' | 'Fri.' | 'Sat.' | 'Sun.'
type Weekend = 'Sat.' | 'Sun.'
type Workday = Exclude<Days, Weekend>
// 结果:
// type Workday = 'Mon.' | 'Tues.' | 'Wed.' | 'Thurs.' | 'Fri.'
此时在 Exclude<Days, Weekend>
中:
Days
是T
,Weekend
是K
。Days
的范围比Weekend
大,从源码逻辑来看结果应该是Days
才对,但实际上却不是。确实有点难理解,没关系,继续往下看 …
在 TypeScript 中有一个「条件类型约束」的概念(详见 官方文档:Conditional Type Constraints):
- 「条件类型检查」通常会给我们提供一些新的约束信息,比如:
- “类型保护缩小(narrowing with type guards)” 给我们一个更具体的类型。
- 类型检查结果为
true
的 “分支” 会进一步约束泛型。
以源码中的 T extends U ? never : T
为栗,我们写个测试代码看看:
type T = 'A' | 'B' | 'C'
type U = 'A' | 'C'
type GuessWhatIam = T extends U ? never : T
// 预期:type GuessWhatIam = 'B'
// 实际:type GuessWhatIam = 'A' | 'B' | 'C'
呃… GuessWhatIam
并不是我们预期的 'B'
,翻车了?并没翻,因为这里的 T extedns U ?
实际上起到的是一个 “类型判定” 的作用,把判定的结果 T
直接赋给了 GuessWhatIam
,就像我们使用三元表达式 const c = a > b ? a : b
一样。
因为这里的 T
和 U
都是具体的某个类型,并不是范式类型(泛型)。而真正能让魔法(进一步约束类型)发生的前提条件是:
T
和U
都得是泛型。
再来一段测试代码:
// 自定义一个 Exclude 工具类型
type CustomExclude<GenericT, GenericU> = GenericT extends GenericU ? never : GenericT
type T = 'A' | 'B' | 'C'
type U = 'A' | 'C'
type GuessWhatIam = CustomExclude<T, U>
// 预期:type GuessWhatIam = 'B'
// 实际:type GuessWhatIam = 'B'
嗯,This is the expected magic 🤒
所以说,要想真正理解 「工具类型」的源码实现,还是得看认真了解官方文档的一些细节,并在实际中检验才行。
8、Extract<T>
/**
* Extract from T those types that are assignable to U
*/
type Extract<T, U> = T extends U ? T : never;
可见跟 Exclude
的源代码差不多,都是通过 “泛型的进一步约束” 来获得更加具体的 T
。
9、NonNullable<T>
/**
* Exclude null and undefined from T
*/
type NonNullable<T> = T extends null | undefined ? never : T;
哈,其实 NonNullable<T>
本质上跟 Exclude<T>
的实现是一样的,只是前者将后者的 K
具体成了 null | undefined
而已。这样就可以通过 “进一步约束泛型” 来排除掉了 T
中原有的 null
或 undefined
了。
10、Parameters<T>
/**
* Obtain the parameters of a function type in a tuple
*/
type Parameters<T extends (...args: any) => any> = T extends (...args: infer P) => any ? P : never;
Parameters<T>
的实现姿势咋一眼看起来跟 Extract<T>
的很像,毕竟这两个「工具类型」都是有点 “提取” 意思在(前者提取 “函数参数类型”,后者提取 “联合类型的子集”)。可再仔细一看,发现有个 infer
关键字看起来好陌生,我们到 官方文档:Inferring Within Conditional Types 看下这玩意儿是个啥?
概括来说:
infer
关键字其实是 TypeScript 提供的一种 “在「条件类型检查中」中声明「待推断类型」变量” 的机制。
结合上面的源码实现来看:
Parameters
限定了T
的范围是(...args: **any**) => any
- 当
T
的范围小于(...args: **infer P**) => any
时,结果为P
;否则,结果为never
。
举倆栗子:
// Example 1
type T = (a: string) => void
type GuessWhatIam = Parameters<T>
// 此时 `T` 参数列表类型实际上是元组 `[a: string]`,
// 所以这个待 infer 的 `P` 是 `[a: string]`
// 即 GuessWhatIam 等效于下面:
// type GuessWhatIam = [a: string]
// Example 2
type T = (a: string, b: number) => void
type GuessWhatIam = Parameters<T>
// 此时 `T` 参数列表类型实际上是元组 `[a: string, b: number]`,
// 所以这个待 infer 的 `P` 是 `[a: string, b: number]`
// 即 GuessWhatIam 等效于下面
// type GuessWhatIam = [a: string, b: number]
11、ReturnType<T>
/**
* Obtain the return type of a function type
*/
type ReturnType<T extends (...args: any) => any> = T extends (...args: any) => infer R ? R : any;
这跟 Parameters
的逻辑基本相同,只不过这次 “待 infer 的变量” 是函数的返回值,而不是参数列表。
四、总结
到这里我们基本上已经理解了 TypeScript 中「工具类型」的使用,也知晓了其源码的大致实现。
是时候说下次见了,Have a nice dinner my friend. 🚀