进阶学习TypeScript之类型体操-基础篇
类型体操是一种调侃的说法,其来源于TypeScript中各种复杂的类型操作。
Hello,大家好,我是BalladJz。近期经常在社区和社群看见很多小伙伴对TypeScript的各种讨论,褒贬不一,我还主动去找了关于TS的话题,看了一些为TS"正名"的文章,再看了评论里的唇枪舌战,真是为博主捏了一把汗,但本文不对TS的'好与不好'做任何评论,但我只能表达我自己是一个TS的重度爱好者
Okay~,回归正文吧,在众多的前端开发开发框架中都能碰见TypeScript身影,react+ts、vue+ts、three.js+ts等,就拿国内近年最火的Vue来说,Vue3也是利用TS编写的,且随着今年底的Vue2停止维护,Vue3的使用频率定然会逐步升高,而利用TS做类型校验的Vue3,TS的大概率会有更多的前端小伙伴使用(这里插入一个额外的话题,有小伙伴知道在Vue2是怎样做类型校验的吗?-- 这里不赘述了 感兴趣的小伙伴可以了解一下flow),而现在还有很多人比较排斥TS
但是!同志,请相信我,认真对待写TS这件事,你必将有意外的收获
(什么TS有更好的开发体验、提示友好、类型错误校验、这些功能就不说了,这是最基本的,说说不常见的)
- 以上图片来自于我随手截的Vue仓库的一段代码。这种代码在基于TS的库中数不胜数,如果你熟悉ts工具类型,看源码定义不再吃力了,从源码的角度理解每一个工具类型的实现机制
- 不再为大佬写的 ts 定义而苦恼了
- 能够在平时的工作中举一反三,规避低级错误,比如函数传参错误、边界情况的判断
- 复杂的大型项目,能够保证数据结构层级不错位,更好的抽象能力,部分枚举、模型和后端保持一致
给大家看一下我曾经面试遇到的两道关于TS类型编程的题 实话讲,作为菜鸡的我是没有做出来的,但它确实难度不高,有趣的是,面试官直接说: "这两道题实际工作中完全用不到,我就想看看你能不能写出来"(那心里叫一个苦啊),当然题目的答案会在本文最后揭晓。
本文将会通过18个类型工具,基本大致是照着官方的文档读下来的(第11题和最后一题面试题答案),记录下来也方便以后自己回顾,会讲解TS类型工具原理和编程技巧。不扯闲篇,全程干货,内容非常多,想提升Ts功力的小伙伴请耐心读下去。相信小伙伴们在读完此文后,能够对这块有更深入的理解。下面,我们开始吧~
当然,本文需要有TS的一些基本功:比如索引访问、映射类型、条件链、keyof、infer、extends(条件类型、继承/拓展、分配的含义)等
1.MyAwaited
Awaited<Type>
对函数await
中的async
操作或 s.then()
上的方法进行建模,根据Promise
对象 返回它的类型,在TS中使用Promise
中的T来描述这个Promise
返回的类型
ts
/**
* desc: 首先利用extends条件判断,是否可以推断(infer)出 Promise的 T类型,如果可以,我们返回它的类型,反之 我们直接返回T
* (以为这就完了吗?显然肯定不是这样的,别忘了await、async出现的原因,它们是为了解决Promise嵌套地域的问题,所以此处我们还需要对多层Promise<T>的处理)
* 也很简单,简单的说就是对这个工具类进行递归,
* 当我们通过 T extends Promise<infer R> 推断出 R的类型后,再次(使用T extends Promise<infer R>)对R进行条件判断
* 如果再次推断出来了R的类型 那证明这个T就是一个嵌套的Promise类型,不管嵌套多少层,依次去进行判断就行
* 当到最后一层时,它定会走 false 分支,最后直接返回我们需要的类型
*/
type MyAwaited<T> = T extends Promise<infer R>
? R extends Promise<any> ? MyAwaited<R> : R
: T
ts
type T1 = MyAwaited<Promise<Promise<Promise<string>>> | boolean> // string | boolean
2.MyPartial
MyPartial<Type>
将所有类型设置为可选
ts
/**
* desc: 传入一个泛型参数,主要通过K extends keyof T约束K必须为T的子类型,
* 再根据 索引访问 该键的类型,最终keyof T得到的是T的所有key组成的联合类型
*/
type MyPartial<T> = {
[P in keyof T]?: T[P]
}
ts
// 准备一个User类型
type User = {
name: string
age: number
isAdult: boolean
children: User
desc: () => any
}
type T2 = MyPartial<User> // 最终的类型是将User的所有类型转换为可选类型
3.MyRequired
MyRequired<Type>
将所有类型设置为必选
ts
/**
* desc: MyRequired 与 MyPartial完全相反 只是在最后利用 映射修饰符 去掉可选条件
*/
type MyRequired<T> = {
[P in keyof T]?: T[P]
}
ts
// 这里的T2的类型经过MyPartial的转换他的所有属性都是可选的
// type T2 = {
// name?: string | undefined;
// age?: number | undefined;
// isAdult?: boolean | undefined;
// children?: any | undefined;
// desc?: (() => void) | undefined;
// }
// 再使用 MyRequired 进行转换一次
type T3 = MyRequired<T2> // 最终的类型是将User的所有类型转换为必选类型
4.MyReadonly
MyReadonly<Type>
将所有属性Type设置为 仅读属性(不可修改)
ts
/**
* desc: 主要实现是通过映射遍历所有key,然后给每个key增加一个readonly修饰符
*/
type MyReadonly<T> = {
readonly [K in keyof T]: T[K]
}
ts
type T4 = MyReadonly<User>
5.MyRecord
MyRecord<Keys, Type>
将其属性键为Keys,属性值为Type。该实用程序可用于将一种类型的属性映射到另一种类型。
ts
/**
* desc: 须对第一个泛型参数中做类型约束,只支持 string | number | symbol
*/
type MyRecord<T extends string | number | symbol, P> = {
[K in T]: P
}
ts
type T5 = MyRecord<string, number> // { [x: string]: number; }
6.MyExclude
MyExclude<UnionType, ExcludedMembers>
UnionType 排除所有可分配给的联合成员来构造类型 ExcludedMembers
ts
/**
* desc: 利用条件分配类型,如果前一个泛型参数的类型,在第二个参数中存在 就返回never
* 如果不存在,就返回 T,最终构造出新的类型
*/
type MyExclude<T, P> = T extends P ? never : T
ts
type ExA = '1' | 2 | { name: string } | symbol | ((...arg: any) => void)
type ExB = '1' | symbol
type T6 = MyExclude<ExA, ExB> // { name: string } | 2 | ((...arg: any) => void)
7.MyExtract
MyExtract<Type, Union>
Type 通过从可分配给的所有联合成员中提取来构造类型Union。
ts
/**
* desc: 与MyExclude完全相反:如果前一个泛型参数中的类型,在第二个泛型参数中存在 就返回T,
* 如果不存在,就返回 never,最终构造出新的类型
*/
type MyExtract<T, P> = T extends P ? T : never
ts
type ExC = '1' | 2 | { name: string } | symbol | ((...arg: any) => void)
type ExD = '1' | Function
type T7 = MyExtract<ExC, ExD> // "1" | ((...arg: any) => void)
8. MyPick
MyPick<Type, Keys>
从 Type 中取出 Key 的属性集
ts
/**
* desc:在泛型参数上须对第二个泛型参数中做类型约束,利用 extends 与 keyof 进行约束和获取第一个泛型的键即可
* 然后根据第二个泛型参数中的类型,从第一个泛型参数中 取出出符合第二个泛型参数的新类型
*/
type MyPick<T, K extends keyof T> = {
[P in K]: T[P]
}
ts
type T8 = MyPick<User, 'name' | 'desc' | 'children'> // { name: string; desc: () => void; children: User; }
9. MyOmit
MyOmit<Type, Keys>
从 Type 中删除 Key 的属性集(与MyPick相反)
ts
/**
* desc:根据第二个泛型参数中的类型,从第一个泛型参数中 排除出符合第二个泛型参数,而构造出的新类型
* 需要注意的是,要排除第一个泛型的参数,需要用到上面的 MyExclude 工具类,从而获取到新的类型
*/
type MyOmit<T, K extends string | number | symbol> = {
[P in MyExclude<keyof T, K>]: T[P]
}
ts
type T9 = MyOmit<User, 'name' | 'age'> // { desc: () => void; children: User; isAdult: boolean; }
值得注意的是:
ts
/**
* 值得注意的是:
* MyPick与MyExtract 和 MyExcludes与MyOmit的区别
*
* MyPick与MyExtract:
* 可以大致理解为,它们都是从取出第一个泛型参数中取出符合第二个泛型参数的键值,且它们的第二个参数都是联合类型
* 但MyPick的第一个参数是一个Object类型,第二个参数是利用 keyof 获取到的Object的键的联合类型
* MyExtract前后两个参数都更适用于联合类型 type T06 = MyExclude<{ name: string, age: number } | never, string> // type T06 = { name: string; age: number; }
*
* MyExclude与MyOmit: 与上面的MyPick与MyExtract 去反向理解即可
*/
10. MyNonNullable
MyNonNullable<Type>
排除 null undefined 构造类型Type
ts
/**
* desc 1.可以根据对T进行约束为 null | undefined 如果 条件判断为真 则返回never 否则 返回我们需要的类型,
* 2.通过上面的阅读,这个不就是对 MyExclude 进行使用吗? 对MyExclude的第二个参数传入null和undefined的联合类型即可
* 3.但实际上还有更简单的方式,利用 &
*/
type MyNonNullable<T> = T extends null | undefined ? never : T
type MyNonNullable1<T> = MyExclude<T, null | undefined>
type MyNonNullable2<T> = T & {}
type T10 = MyNonNullable<string | Array<number> | undefined> // string | number[]
type T11 = MyNonNullable1<string | Array<number> | undefined> // string | number[]
type T12 = MyNonNullable2<string | Array<number> | undefined> // string | number[]
11. MyReturnType
MyReturnType<Type>
获取函数的返回值类型
ts
/**
* desc 此工具类需要对Type进行约束为一个Function类型,对其返回值使用infer关键字进行类型推断,且在推断时 需要使用typeof去获取到Function的类型
* 如果可以可以推断的话,那就返回新的类型,反之 返回never,最终根据构造成一个新的联合类型
*/
type MyReturnType<T extends (...arg: any) => any> = T extends (...arg: any) => infer R ? R : never
ts
// 此处准备两个函数
const fn1 = (a: boolean, b: number, c: string): number | string | void => { }
const fn2 = () => {
return { name: 'zs', age: 18 }
}
type T13 = MyReturnType<typeof fn1> // string | number | void
type T14 = MyReturnType<typeof fn2> // { name: string, age: number }
12. MyParameters
MyParameters<Type>
获取函数的参数类型 并构成一个新的元组类型
ts
/**
* desc MyParameters与MyReturnType很相似,也需要对Type进行约束为一个Function类型,而在推断时则不是推断返回值了,而是推断参数
* 如果可以推断的话,那就返回新的类型,反之 返回never,最终根据构造成一个参数类型元组
*/
type MyParameters<T extends (...arg: any) => any> = T extends (...arg: infer R) => any ? R : never
ts
const fn1 = (a: boolean, b: number, c: string): number | string | void => { }
const fn2 = () => {
return { name: 'zs', age: 18 }
}
type T15 = MyParameters<typeof fn1> // [a: boolean, b: number, c: string]
type T16 = MyParameters<typeof fn2> // []
13. MyConstructorParameters
MyConstructorParameters<Type>
可以获取类的构造函数的参数类型,存在一个元组中
ts
/**
* desc: 此方法与MyParameters 很相似,只是在此处要对T约束为 abstract 抽象类,通过infer 推断出类的constructor参数类型,并得到一个元组
* 此处有两个重点 ==> 请看下面说明
*/
class Person {
constructor(name: string, age: number, isAdult: boolean) {
this.name = name;
this.isAdult = isAdult;
}
name: string = 'zs';
isAdult: boolean = false;
}
type MyConstructorParameters<T extends abstract new (...args: any) => any> = T extends abstract new (...args: infer R) => any
? R
: never
ts
type T17 = MyConstructorParameters<typeof Person> // [name: string, age: number, isAdult: boolean]
type T18 = MyConstructorParameters<any> // unknown[]
14. MyInstanceType
MyInstanceType<Type>
可以获取类的构造函数的实例的类型,组成一个新的类型Type。
ts
/**
* desc: 反向理解MyConstructorParameters 无非是使用infer R的位置不一样
* 通过infer 推断构造函数的实例 即可
*/
type MyInstanceType<T extends abstract new (...args: any) => any> = T extends abstract new (...args: any) => infer R ? R : any
ts
type T19 = InstanceType<typeof Person> // { name: string, isAdult: boolean }
const BalladJz: T19 = {
name: 'ballad_jz',
isAdult: true
}
说明
ts
class OrdinaryClass { }
abstract class AbstractClass { }
const c1: typeof OrdinaryClass = OrdinaryClass // 可以赋值
const c2: typeof OrdinaryClass = AbstractClass // 不能赋值 => 无法将抽象构造函数类型分配给非抽象构造函数类型。
const c3: typeof AbstractClass = OrdinaryClass // 可以赋值
const c4: typeof AbstractClass = AbstractClass // 可以赋值
/**
* 由此看出,如果将类型定义为抽象类(抽象构造函数),则既可以赋值为抽象类,也可以赋值为普通类;而反之则不行。
*/
/**
* 定义一个类
*/
class People {
name: string = 'ww';
age: number = 18;
constructor() { }
}
const p1: People = new People(); // p1可以正常赋值
const p2: People = People; // 等号后面的People报错,类型"typeof People"缺少类型"People"中的以下属性: name, age
const p3: typeof People = new People(); // p3报错,类型 "People" 中缺少属性 "prototype",但类型 "typeof People" 中需要该属性
const p4: typeof People = People; // p4可以正常赋值
/**
* 由此看出
* 当把类直接作为类型时,该类型约束的是该类型必须是类的实例;即该类型获取的是该类上的实例属性和实例方法(也叫原型方法)
* 当把typeof 类作为类型时,约束的满足该类的类型;即该类型获取的是该类上的静态属性和方法。
*/
15. MyThisParameterType
MyThisParameterType<Type>
提取函数类型的this参数的类型,如果函数类型没有参数,则提取未知this。
ts
/**
* desc: 此处需要利用infer进行类型推断,如果可以推断出this的类型,则直接返回this的类型,否则返回 unknown
*/
type MyThisParameterType<T> = T extends (this: infer R, ...args: any) => any ? R : unknown
ts
// 定义一个函数
function toFunThisType(this: { time: number; address: string }) {
return this
}
type T20 = MyThisParameterType<typeof toFunThisType> // { time: number; address: string; }
16. MyOmitThisParameter
MyOmitThisParameter<Type>
剔除 this 参数 MyThisParameterType
ts
// 定义三个函数
function toWipeFunctionThisType_1(this: string, a: number) {
return this + a
}
function toWipeFunctionThisType_2(a: string) {
return a
}
function toWipeFunctionThisType_3() { }
/**
* desc: 如果传递的函数不包含this参数,则直接返回函数的类型;如果包含this参数,则剔除this参数 返回不带this类型的函数的类型
* MyOmitThisParameter 需要结合到 MyThisParameterType 进行条件判断, (如何判断是否存在this类型 请看下图)
* ① 如果 MyThisParameterType 推断出来了是 unknown,那就证明这个函数没有this的类型,我们直接返回 typeof T 函数的类型
* ② 反之,就证明这个函数存在this类型 则我们直接执行 T extends (...args: infer A) => infer R ? (...args: A) => R : T
* 再使用infer 推断出参数的类型和返回值的类型,如果可以推断出来就返回 剔除函数参数this类型后的类型,否则就返回 T
*
*/
/** 定义一个判断函数参数是否有this类型,如果没有函数的类型,如果有就返回 '有this类型'字面量 */
type IsExistThisType<T> = unknown extends ThisParameterType<T> ? T : '有this类型'
type N1 = IsExistThisType<typeof toWipeFunctionThisType_1> // '有this类型' --> toWipeFunctionThisType_1 他是有this类型的
type N2 = IsExistThisType<typeof toWipeFunctionThisType_2> // (a: string) => string --> toWipeFunctionThisType_2 是没有this类型的 通过typeof直接返回的函数类型
type N3 = IsExistThisType<typeof toWipeFunctionThisType_3> // () => void --> toWipeFunctionThisType_3 也是没有this类型的 通过typeof直接返回的函数类型
// 根据上面的案例,糅合一下,就得到了我们的 MyOmitThisParameter
type MyOmitThisParameter<T> = unknown extends ThisParameterType<T> ? T : T extends (...args: infer A) => infer R ? (...args: A) => R : T
ts
type T21 = MyOmitThisParameter<typeof toWipeFunctionThisType_1> // (a: number) => string
type T22 = MyOmitThisParameter<typeof toWipeFunctionThisType_2> // (a: string) => string
type T23 = MyOmitThisParameter<typeof toWipeFunctionThisType_3> // () => void
17. ThisType
ThisType<Type>
此实用工具不返回转换后的类型。相反,它充当上下文 this 类型的标记。
ts
// 比如 有一个对象类型 MyObject ,它有一个属性 name 和一个方法 getName ,该方法返回对象的 name 属性值:
type MyObject = {
name: string;
getName(): string;
};
// 我们可以再创建一个新的对象类型 MyNewObject ,它与 MyObject 类型相同,但在 getName 方法中使用 this 关键字来引用对象本身。
type MyNewObject = ThisType<{ name: string }> & {
getName(this: MyNewObject): string;
};
// 我们可以使用 ThisType<Type> 工具类型来实现这个目标
type MyNewObject_1 = {
name: string;
getName(this: MyNewObject_1): string;
}
// 可以看到, MyNewObject 类型与 MyObject 类型相同,只是在 getName 方法中使用了 this 关键字来引用对象本身。这是通过使用 ThisType<Type> 工具类型来实现的,其中 Type 参数指定了 this 关键字的类型。
18. MyIsIncludes
ts
/**
* question:MyIsIncludes 在类型系统里面实现 js中 Array.includes 方法,这个类型接受两个参数,如果第1个参数包含了第2个参数,那么返回true,否则返回false
*
* answer: 1.此工具类需要对泛型做一个约束,先通过 extends 约束T为一个Array类型
* 2.再根据 extends 分配出第一个参数的第一项和剩余项,通过infer推断,
* 3.如果不能推断出来直接返回 false
* 4.如果可以推断出来,再利用 extends 进行第一个参数的第一项和第二个参数的判断是否是同一类型
* 如果是同一个类型,那我们直接返回 true
* 如果不是同一类型,则继续递归 MyIsIncludes,直至把第一个参数的所有项与第二个参数判断完成,仍没有的话 那就返回false
* over~
*
* 如下:
*/
type MyIsIncludes<T extends Array<unknown>, U> = T extends [infer F, ...infer R] ? (F extends U ? true : MyIsIncludes<R, U>) : false
type T24 = MyIsIncludes<['1', '2'], '3'> // false
type T25 = MyIsIncludes<['1', '2'], '2'> // true
以为这就完了吗?上面的 MyIsIncludes是可以推出大部分类型,但在类型系统里,是分层级的
比如:比如顶层的any unknown,中间的string, number, 底层的never,他们之间的类型全都判断还是存在一定问题的
ts
// 不完善的工具:MyIsIncludes 此处的 F extends U 并不能完全判断出 类型相等 (此处的判断是全等判断)
// 比如:
type T26 = MyIsIncludes<[string, 2], any> // true
type T27 = MyIsIncludes<[true, 2], boolean> // true
// T26 T27的类型都是 true (这显然不是想要的结果)
// 当然这个不只是string和any、true/false与boolean有这个问题,也包括readonly、可选属性等
优化后
ts
/**
* 只需要对(基于F extends U)这块儿判断进行优化一下即可
* 它的基本骨架是type IfEquals<> = (参数1) extends (参数2) ? A : B (这里的A B没有实际意义,随便给个字符串和数字都行,只是利于我们判断类型是否全等)
* 如果第一个参数能够分配给第二个参数那就返回A 如果 不能那就返回B,
* 最终我们再判断 A extends B 就完成了 F extends U 的优化
*
*/
type IsEqual<T, U> = (<G>() => G extends T ? '1' : '2') extends (<G>() => G extends U ? '1' : '2') ? true : false
type CheckIsEqual_1 = IsEqual<true, boolean> // false
type CheckIsEqual_2 = IsEqual<{ name: string }, { readonly name: string }> // false
type CheckIsEqual_3 = IsEqual<string, string> // true
// 再把我们的 IsEqual 替换上面的 F extends U
type MyIsIncludes<T extends Array<unknown>, U> = T extends [infer F, ...infer R]
? (IsEqual<F, U> extends true ? true : MyIsIncludes<R, U>)
: false
type T28 = MyIsIncludes<[string, 2], any> // false
type T29 = MyIsIncludes<[true, 2], boolean> // false
type T30 = MyIsIncludes<[1, 2], 1> // true
type T31 = MyIsIncludes<['Kars', 'Esidisi', 'Wamuu', 'Santana'], 'Dio'> // false
type T32 = MyIsIncludes<['Kars', 'Esidisi', 'Wamuu', 'Santana'], 'Kars'> // true
写在最后
如果大家对TS感兴趣可以一起去做一下 type-challenges 里面两百多道关于TS类型的题