本文目标:
- 更加深入的理解和掌握泛型
- 更加熟练这些内置工具类型在项目中的运用
Exclude
Exclude<T, U>:作用简单说就是把 T 里面的 U 去掉,再返回 T 里还剩下的。T 和 U 必须是同种类型(具体类型/字面量类型)。如下
1 | type T1 = Exclude<string | number, string>; |
怎么就剩个 a | c 了?这怎么执行的?
先看一张图
三元表达式大家都知道,不是返回 a 就是返回 b,这么算的话,这个 some 的类型应该是 b 才对呀,可这个结果是 a | b 又是怎么回事呢,这都是由于 TS 中的拆分或者说叫分发机制导致的
简单说就是联合类型并且是裸类型就会产生分发,分发就会把联合类型中的每一个类型单独拿去判断,最后返回结果组成的联合类型,a | b 就是这么来的,这个特性在本文后面会提到多次所以铺垫一下,这也是为什么反 Exclude 放在开头的原因
结合 Exclude 的实现和例子来理解下
1 | // 源码定义 |
上面例子中的执行逻辑:
- 由于分发会把联合类型中的每一个类型单独拿去判断的原因,会先把
T,也就是前面a | b | c给拆分再单独放入T extends U ? never : T判断 - 第一次判断
a(T就是a),U就是b | d,T并没有继承自U,判断为假,返回T也就是a - 第二次判断放入
b判断为真,返回never,ts中的never我们知道就是不存在值的意思,连undefined都没有,所以never会被忽略,不会产生任何效果 - 第三次判断放入
c,判断为假,和a同理 - 最后将每一个单独判断的结果组成联合类型返回,
never会忽略,所以就剩下a | c
总之就是:如果
T extends U满足分发的条件,就会把所有单个类型依次放入判断,最后返回记录的结果组合的联合类型
Extract
Extract<T, U>:作用是取出 T 里面的 U ,返回。作用和 Exclude 刚好相反,传参也是一样的
看例子理解 Extract
1 | type T1 = Extract<'a' | 'b' | 'c', 'a' | 'd'>; |
和 Exclude 源码对比也只是三元表达式返回的 never : T 对调了一下,执行原理也是一样一样儿的,就不重复了
Omit
Omit<T, K>:作用是把 T(对象类型) 里边的 K 去掉,返回 T 里还剩下的
Omit 的作用和 Exclude 是一样的,都能做类型过滤并得到新类型。
不同的是 Exclude 主要是处理联合类型,且会触发分发,而 Omit 主要是处理对象类型,所以自然的这俩参数也不一样。
用法如下
1 | // 这种场景 type 和 interface 是一样的,后面就不重复说明了 |
源码定义
1 | // keyof any 就是 string | number | symbol |
- 首先第一个参数
T要传对象类型,type或interface都可以 - 第二个参数
K限制了类型只能是string | number | symbol,这一点跟js里的对象是一个意思,对象类型的属性名只支持这三种类型 in是映射类型,用来映射遍历枚举类型。大白话就是循环、循环语法,需要配合联合类型来对类型进行遍历。in的右边是可遍历的枚举类型,左边是遍历出来的每一项- 用
Exclude去除掉传入的属性后,再遍历剩下的属性,生成新的类型返回
示例解析:
1 | type User = { |
我们调用 Omit 传入的参数是正确的,所以就分析一下后面的执行逻辑:
Exclude<keyof T, K>等于Exclude<'name'|'age'|'gender', 'age'>,返回的结果就是'name'|'gender- 然后遍历
'name'|'gender',第一次循环P就是name,返回T[P]就是User['name'] - 第二次循环
P就是gender,返回T[P]就是User['gender'],然后循环结束 - 结果就是
{ name: string, gender: string }
Pick
Pick<T, K> :作用是取出 T(对象类型) 里边儿的 K,返回。
好像和 Omit 刚好相反,Omit 是不要 K ,Pick 是只要 K
传参方式和 Omit 是一样的,就不赘述了,用法示例:
1 | type User = { |
源码定义
1 | type Pick<T, K extends keyof T> = { [P in K]: T[P]; } |
- 可以看到等号左边做了泛型约束,限制了第二个参数
K必须是第一个参数T里的属性。 - 如果第二个参数传入联合类型,会触发分发,以此来确保准确性,联合类型中的每一个单独类型都必须是第一个对象类型中的属性(不限制的话右边就要出错了)
- 参数都正确之后,等号右边的逻辑其实就是和
Omit一模一样的了,直接遍历K,取出返回就完事儿了
练习一
请利用本文上述内容完成:基于如下类型,实现一个去掉了 gender 的新类型,实现方法越多越好
1 | type User = { |
这个?
1 | type T1 = { name: string, age: number } |
???
我写了几个,欢迎补充:
1 | type T1 = Omit<User, 'gender'> |
Record
Record<K, T>:作用是自定义一个对象。K 为对象的 key 或 key 的类型,T 为 value 或 value 的类型。
你有没有这样用过 ↓
1 | const obj:any = {} |
反正我有,其实用 Record 定义对象,在工作中还是很好用的,而且非常灵活,不同的对象定义上也会有一点区别,如下
空对象
1 | // never,会限制为空对象 |
任意对象
1 | // 任意对象,unknown 或 {} 表示对象内容不限,空对象也行 |
自定义对象 key
1 | type keys = 'name' | 'age' |
自定义对象 value
1 | type keys = 'a' | 'b' |
源码定义
1 | type Record<K extends any, T> = { [P in K]: T; } |
左边限制了第一个参数 K 只能是 string | number | symbol 类型,可以是联合类型,因为右边遍历 K 了,然后遍历出来的每个属性的值,直接赋值为传入的第二个参数
Partial
Partial<T>:作用生成一个将 T(对象类型) 里所有属性都变成可选的之后的新类型
示例如下:
1 | type User = { |
源码定义
1 | type Partial<T> = { [P in keyof T]?: T[P]; } |
这下看源码定义的是不是特别简单,就是循环传进来的对象类型,给每个属性加个 ? 变成可选属生
Required
Required<T>:作用和 Partial<T> 刚好相反,Partial 是返回所有属性都是非必填的对象类型,而 Required 则是返回所有属性都是必填项的对象类型。参数 T 也是一个对象类型。
示例:
1 | type User = { |
源码定义
1 | type Required<T> = { [P in keyof T]-?: T[P]; } |
和 Partial 的源码定义相比基本一样的,只是这里多了个减号 -,没错,就是减去的意思,-? 就是去掉 ?,然后就变成必填项了,这样解释是不是很好理解
Readonly
Readonly<T> :作用是返回一个所有属性都是只读不可修改的对象类型,与 Partial 和 Required 是非常相似的。参数 T 也是一个对象类型。
示例:
1 | type User = { |
怎么样?看到这是不是越发觉得源码的类型定义越看越简单了
我:那是不是说把所有只读类型,全都变成非只读就只需要 -readonly 就行了?
你:是的,说得很对,就是这样的
练习二
从上面几个工具类型的源码定义中我们可以发现,都只是简单的一层遍历,就好像 js 中的浅拷贝,比如有下面这样一个对象
1 | type User = { |
要把这样一个对象所有属性都改成可选属性,用 Partial 就行不通了,它只能改变第一层,children 里的所有属性都改不了,所以请写一个可以实现的类型,功能类似深拷贝的意思
先稍微想想再往下看答案哟
写出来一个的话,Partial 、Required 、 Readonly 的 “深拷贝” 类型是不是就都有了呢
想一下
1 | // Partial 源码定义 |
外层再加了一个三元表达式,如果不是对象类型直接返回,如果是就遍历;然后属性值改成递归调用就可以了
1 | // 递归 Required |
NonNullable
NonNullable<T>:作用是去掉 T 中的 null 和 undefined。T 为字面量/具体类型的联合类型,如果是对象类型是没有效果的。如下
1 | type T1 = NonNullable<string | number | undefined>; |
源码定义
1 | // 4.8版本之前的版本 |
TS 4.8版本 之前的就是用一个三元表达式来过滤 null | undefined。而在 4.8 版本直接就是 T & {},这是什么原理呢?其实是因为这个版本对 --strictNullChecks 做了增加,这主要体现还是在联合类型和交叉类型上,为什么这么说?
在 js 中都知道万物皆对象,原型链的最终点的正常对象就是 Object 了(null 算不正常的),数据类型都是在原型链中继承于 Object 派生出来的。
在 ts 中也一样,由于 {} 是一个空对象,所以除了 null 和 undefined 之外的基础类型都可以视作继承于 {} 派生出来的。或者说如果一个值不是 null 和 undefined 就等于 这个值 & {} 的结果,如下
1 | type T1 = 'a' & {}; // 'a' |
如果 T & {} 中的 T 不是 null/undefined 就可以认为它肯定符合 {} 类型,就可以把 {} 从交叉类型中去掉了,如果是,则会被判为 never,而 never 是会被忽略的(上面 Exclude 源码定义里有提到),所以在结果里自然就排除掉了 null 和 undefined。
还有如果 T & {} 中的 T 是联合类型,是会触发分发的,这个就不再解释了
练习三
请实现一个能去掉对象类型中 null 和 undefined 的类型
1 | // 需要把如下类型变成 { name: string } |
这里出现了一个本文第一次出现的关键字 as,我们知道它可以用来断言,在 ts 4.1 版本可以在映射类型里用 as 实现键名重新映射,达到过滤或者修改属性名的目的,如果指定的类型解析为 never 时,会被忽略不会生成这个属性
如上只能过滤对象第一层的 null 和 undefined
如何更进一步改成可以递归的呢?
1 | type User = { |
Awaited
Awaited<T>:作用是获取 async/await 函数或 promise 的 then() 方法的返回值的类型。而且自带递归效果,如果是这样嵌套的异步方法,也能拿到最终的返回值类型
示例:
1 | // Promise |
来看下源码定义,看下到底是怎么执行的,是怎么拿到结果的呢?
1 | // 源码定义 |
泛型条件有点多,就换了下行,方便看
如果
T是null或undefined就直接返回T如果
1
T
是对象类型,并且里面有
1
then
方法,就用
1
infer
类型推断出
1
then
方法的第一个参数
1
onfulfilled
的类型赋值给
1
F
,
1
onfulfilled
其实就是我们熟悉的
1
resolve
。所以这里可以看出或者准确的说,
1
Awaited
拿的不是
1
then()
的返回值类型,而是
1
resolve()
的返回值类型
既然
1
F
是回调函数
1
resolve
,就推断出该函数第一个参数类型赋值给
1
V
,
1
resolve
的参数自然就是返回值
- 传入
V递归调用
- 传入
F不是函数就返回never
如果
T不是对象类型 或者 是对象但没有then方法,返回T,就是最后一行的T
Parameters
Parameters<T>:作用是获取函数所有参数的类型集合,返回的是元组。T 自然就是函数了
使用示例:
1 | declare function f1(arg: { a: number; b: string }): void; |
可以看到限制了函数类型,然后 ...args 取参数和 js 中的用法是一样的,infer 表示待推断的类型变量,打断出 ...args 取到的类型赋值给 P
ReturnType
ReturnType<T>:作用是获取函数返回值的类型。T 为函数
示例:
1 | declare function f1(): { a: number; b: string }; |
可以看到源码定义上和 Parameters 是基本一样的,只是把类型推断的参数换成返回值了
ConstructorParameters/InstanceType
我们知道 Parameters 和 ReturnType 这一对是获取普通/箭头函数的参数类型集合以及返回值类型的了,还有一对组合ConstructorParameters 和 InstanceType 是获取构造函数的参数类型集合以及返回值类型的,和上面的比较类似我就放到一起了
Uppercase/Lowercase
这俩儿的作用是转换全部字母大小写
1 | type T1 = Uppercase<"abcd"> |
Capitalize/Uncapitalize
这俩儿的作用是转换首字母大小写
1 | type T1 = Capitalize<"abcd efg"> |
练习四
请实现一个类型,把对象类型中的属性名换成大写,需要注意的是对象属性名支持 string | number | symbol 三种类型
1 | type User1 = { |
综合练习
请实现一个类型,可以把下划线属性名的对象,换成驼峰属性名的对象。这个就没有现成的工具类型调用了,所以需要我们额外实现一个
这个练习用到了本文中的很多知识,先自己写一下咯
1 | type User1 = { |
这个练习用到了 extends、infer、as、循环、递归,相信能更好地帮助我们理解和运用
参考资料
www.typescriptlang.org/docs/handbo…
原作者:沐华
链接:https://juejin.cn/post/7170662948656906253
来源:稀土掘金