likes
comments
collection
share

Typescript学习(十一) 内置工具类型

作者站长头像
站长
· 阅读数 46

前面我们学习了类型工具、类型层级、泛型等概念, 并用它们组装了一些工具类型,其实在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等等;

  1. 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;
  }
*/
  1. 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)的子类; 再用映射类型获取键的类型, 利用索引类型查询获取值类型;

  1. 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正是利用了这种规律, 实现了删除特定成员的功能

  1. 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第二个泛型参数, 规定了必须是第一个参数的键组成的联合类型的子类型;

集合工具类型

集合工具类型, 是利用了条件类型、以及条件类型分布式特性组成的工具类型, 其特点是利用集合的逻辑, 解决现实问题;

所谓的集合, 其实是数学中的一个概念, 其中涉及交集、并集、差集、补集

  1. 交集, 假如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则刚好相反, 如果这个成员是第二个泛型参数的子集, 则返回该成员! 当然, 求交集, 也可以使用交叉类型, 就更加简单了;

  1. 并集, 所谓的并集, 就是, 你和我都具备的所有成员之和, 关于并集其实没什么工具类型, 毕竟这个太简单了, 联合类型的结果, 其实就是两个类型的并集
type union<T, U> = T | U

type A = 1 | 2

type B = 2 | 3

type C = union<A, B> // 1 | 2 | 3
  1. 差集, 其实前面已经说过了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
  1. 补集, 所谓的补集, 其实就是差集的特殊形式, 两个类型, 如何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
评论
请登录