Typescript学习(十一) 内置工具类型
前面我们学习了类型工具、类型层级、泛型等概念, 并用它们组装了一些工具类型,其实在Typescript中, 系统是内置了很多工具类型的, 利用好它们, 可以提高我们的编程效率, 学习它们, 也有利于加深我们对现有知识的理解和掌握;
属性修饰工具类型
先来看看第一个, 属性修饰工具类型, 我们之前在学习对象类型的时候, 知道我们可以对一个对象的属性修饰为可选和只读, 而属性修饰工具类型, 正是用来专门修改属性修饰符的; 内置的工具类型有Partial、Readonly、Required
type Animal = {
name: string;
color: string;
food: string;
}
// 1. 将对象类型中的所有成员设置为可选
type Partial<T> = {
[P in keyof T]?: T[P]
}
type PartialResult = Partial<Animal>
/**
* type PartialResult = {
name?: string | undefined;
color?: string | undefined;
food?: string | undefined;
}
*/
// 2. 将对象类型的所有成员设置为只读
type Readonly<T> = {
readonly [P in keyof T]: T[P]
}
type ReadonlyResult = Readonly<Animal>
/**
* type ReadonlyResult = {
readonly name: string;
readonly color: string;
readonly food: string;
}
*/
// 3. 将所有成员变为必选
type Required<T> = {
[P in keyof T]-?:T[P]
}
type RequiredType = Required<PartialResult>
/**
* type RequiredType = {
name: string;
color: string;
food: string;
}
*/
// 4. 将只读属性变为可编辑属性, 这个工具类型并非内置
type Editable<T> = {
-readonly [P in keyof T]: T[P]
}
type EditableResult = Editable<ReadonlyResult>
/**
* type EditableResult = {
name: string;
color: string;
food: string;
}
*/
以上方法, 其实主要利用了类型工具中的索引类型(包括, 索引签名类型、索引类型查询)和映射类型, 相当于对一个对象类型的属性进行了遍历, 然后, 每个属性的修饰符进行修改, 我们可以利用?或者+?来将一个属性设置为可选; 利用-?来将一个属性的可选修饰符去掉, 使之变为必选属性; 同样的方式也适用于readonly, 即 是否只读;
来看看使用:
// 1. 正确, 一个属性都没有, 也不会报错, 因为属性全部都是只读的
let partialObj:PartialResult = {}
// 2.报错, color为只读属性
let readonlyObj:ReadonlyResult = {
name: '阿黄',
color: 'yellow',
food: 'bone'
}
readonlyObj.color = 'green'
// 3.报错, 缺少了food属性, 而这是必填属性
let requiredObj:RequiredResult = {
name: '阿黑',
color: 'black'
}
// 4.正确, 属性已经变更为了非只读, 可以修改
let editableObj:EditableResult = {
name: '泰迪',
color: 'brown',
food: 'bone'
}
editableObj.food = 'meat'
结构工具类型
结构工具类型相比于属性修饰工具类型, 要更复杂一些, 属性修饰工具类型只是改改属性修饰符; 而结构工具类型则是直接对一个类型的结构动手, 对其进行新增和修改, 用到的类型工具有: 条件类型、索引类型(索引签名类型, 索引类型查询, 索引类型访问) 、映射类型; 内置的结构工具类型有Record、Pick、Omit、Exclude等等;
- Record
先来看看Record, 这个工具类型可以说在许多源码中出镜率非常高了, 即快速创建一个对象类型
// 第一个泛型参数为键类型;第二个为值类型
type Record<K extends keyof any, T> = {
[P in K]: T
}
type NewType1 = Record<string, string>
/**
* type NewType1 = {
[x: string]: string;
}
*/
其第一个参数也可以是联合类型, 生成的对象类型将会依次以联合类型的成员为属性类型
type NewType2 = Record<string|number, string>
/**
* type NewType2 = {
[x: string]: string;
[x: number]: string;
}
*/
同样, 也能和之前介绍的属性修饰工具类型嵌套使用
type Dog = Readonly<Record<'name'| 'food'|'color', string>>
/**
* type Dog = {
readonly name: string;
readonly food: string;
readonly color: string;
}
*/
- Pick
pick, 正如其字面意思, 就是'选择'其中某个或者某几个属性, 并让其组成一个新的类型
interface Children {
name:string;
age:number;
gender:'male'|'female';
score: number;
grade: number
}
type Pick<T, K extends keyof T> = {
[P in K]:T[P]
}
type PersonType = Pick<Children, 'name'|'age'|'gender'>
/**
* type PersonType = {
name: string;
age: number;
gender: 'male' | 'female';
}
*/
本案例中, 我们使用Pick工具类型, 选择了Children中的几个属性, 并让其组成了一个新的对象类型; 其原理就是利用条件类型, 限定第二个泛型参数必须为第一个泛型的键组成的联合类型(使用了索引类型查询keyof)的子类; 再用映射类型获取键的类型, 利用索引类型查询获取值类型;
- Exclude
第一个泛型参数表示一个类型, 第二个泛型参数指定几个类型成员; 逻辑就是从一个类型中, 删除指定的几个类型成员!
// 将T中, 属于U的属性全部去掉
type Exclude<T, U> = T extends U ? never : T
type Union1 = Exclude<1|2|3, 3> // 1|2
type Union2 = Exclude<1|2|3|4|5, 1|2|3> // 4|5
其实结合之前我们学习的条件类型的分布式特性就能知道, 如果T和U都是联合类型, 且通过泛型的形式传入, 且未被包裹, 那么T必然会将每个成员都依次和U进行判断比较, 然后将每次比较的结果取出来, 组成一个新的联合类型! Exclude正是利用了这种规律, 实现了删除特定成员的功能
- Omit
Omit的作用和Pick相反, Pick是保留指定的属性, 而Omit是删除指定的属性; 但是从实现原理上来看, 其实它又是Pick和Exclude的一个结合体
type Omit<T, K extends keyof any> = Pick<T, Exclude<keyof T, K>>
interface Person {
name:string;
age: number;
gender: 'male' | 'female'
}
type NewType = Omits<Person, 'gender'>
/**
* type NewType = {
name: string;
age: number;
}
*/
其实现逻辑很简单, 先用Exclude<keyof T, K>, 根据K删除掉keyof T中的成员; 然后再用Pick获取剩余的属性;
注意, Pick第二个泛型参数, 规定了必须是第一个参数的键组成的联合类型的子类型;
集合工具类型
集合工具类型, 是利用了条件类型、以及条件类型分布式特性组成的工具类型, 其特点是利用集合的逻辑, 解决现实问题;
所谓的集合, 其实是数学中的一个概念, 其中涉及交集、并集、差集、补集
- 交集, 假如A和B是两个集合, 那么A有, B也有的, 那么共有部分称之为交集, 求交集的内置工具类型有Extract
type Extract<T, U> = T extends U ? T : never
type A = 1 | 2 | 3
type B = 2 | 3 | 4
type C = Extract<A, B> // 2 | 3
这个Extract, 的实现逻辑很像前面的Exclude, 只是逻辑反过来了, Exclude是如果第一个泛型参数的某个成员 是第二个泛型参数的子集, 则取never, 反之则返回这个成员; 而Extract则刚好相反, 如果这个成员是第二个泛型参数的子集, 则返回该成员! 当然, 求交集, 也可以使用交叉类型, 就更加简单了;
- 并集, 所谓的并集, 就是, 你和我都具备的所有成员之和, 关于并集其实没什么工具类型, 毕竟这个太简单了, 联合类型的结果, 其实就是两个类型的并集
type union<T, U> = T | U
type A = 1 | 2
type B = 2 | 3
type C = union<A, B> // 1 | 2 | 3
- 差集, 其实前面已经说过了Exclude其实就是一个求差集的工具类型, 还有就是所谓的差集, 是一个相对的概念, 说白了就是我有你没有的, 就是我相对于你的差集, 反之, 就是你相对于我的差集!
type Exclude<T, U> = T extends U ? never : T
type A = 1 | 2 | 3
type B = 3 | 4 | 5
// A1 就是A相对于B的差集
type A1 = Exclude<A, B> // 1 | 2
// B1 就是B相对于A的差集
type B1 = Exclude<B, A> // 4 | 5
- 补集, 所谓的补集, 其实就是差集的特殊形式, 两个类型, 如何A完全包含了B, 那么, A有B没有的部分, 可以说是A相对于B的差集, 也可以说 这部分就是补集
type complement<T, U extends T> = T extends U ? never : T
type A = 1 | 2 | 3
type B = 1 | 2
type C = complement<A, B> // 3
模式匹配工具类型
所谓的模式匹配工具类型, 其实就是利用之前说的条件类型中的infer关键字, 来匹配到对应位置的类型, 这种就有点类似于正则表达式的匹配思路
// 定义一个通用的函数类型
type FunctionType = (...args: any[]) => any
// 获取函数参数类型
type ParamsType<T extends FunctionType> = T extends (...args: infer R) => any ? R : never
// 测试函数
type MyFn = (num: number, str:string) => boolean
// 参数的类型
type Result1 = ParamsType<MyFn> // [num: number, str: string]
// 获取函数返回值
type ReturnTypes<T extends FunctionType> = T extends (...args: any[]) => infer R ? R : never
// 返回值的类型
type Result2 = ReturnTypes<MyFn> // boolean
上面的案例中, 我们使用infer模式匹配到了一个函数的参数以及返回值的类型; infer的灵活度远不止此, 还可以对原来的ParamsType进行工具类型进行改造, 使之获取首个参数的类型!
// 获取函数首个参数类型
type ParamsType<T extends FunctionType> = T extends (first: infer F, ...rest: any[]) => any ? F : never
// 测试函数
type MyFn = (num: number, str:string) => boolean
// 参数的类型
type Result1 = ParamsType<MyFn> // number
还能当返回一个对象的时候, 获取这个返回对象的键组成的联合类型
// 定义一个通用的函数类型
type FunctionType = (...args: any[]) => any
// 获取用户信息
type UserInfo = (idCardNo: number) => {name: string, age: number, gender: 'male' | 'female'}
// 如果返回值是一个对象, 则获取该返回值的键的联合类型
type ReturnKeyTypes<T extends FunctionType> = T extends (...args: any[]) => infer R ? (R extends object ? keyof R : 0) : never
// 返回值的类型
// type Result2 = ReturnTypes<MyFn> // boolean
type Result2 = ReturnKeyTypes<UserInfo> // "name" | "age" | "gender"
除了函数, 还能对class进行同样的模式匹配
// 定义一个通用class类型
type ClassType = abstract new (...args: any[]) => any;
// 获取构造器的参数的类型
type ConstructorParams<T extends ClassType> = T extends abstract new (...args: infer R) => any ? R : never
// 测试的类
type AnimalClassType = abstract new (...names: string[]) => number[];
type Result1 = ConstructorParams<AnimalClassType> // string[]
// 获取构造器的返回值的类型
type ConstructorReturn<T extends ClassType> = T extends abstract new (...args: string[]) => infer P ? P : never
type Result2 = ConstructorReturn<AnimalClassType> // number
前面我们对返回值是一个对象的函数类型进行模式匹配, 获取了其返回值的键的联合类型, 看上去其实是分了两步: 先判断R是否存在, 再判断R是否是对象, 如果是, 则keyof , 其实这两步可以合并, 直接在infer的时候, 判断其是否是对象类型
// 如果返回值是一个对象, 则获取该返回值的键的联合类型
type ReturnKeyTypes<T extends FunctionType> = T extends (...args: any[]) => (infer R extends object) ? keyof R : never
// type ReturnKeyTypes<T extends FunctionType> = T extends (...args: any[]) => infer R ? (R extends object ? keyof R : 0) : never
type Result2 = ReturnKeyTypes<UserInfo> // "name" | "age" | "gender"
转载自:https://juejin.cn/post/7267922824664350761