【每日一拳】打在深度学习 Typescript 之掌握+手写内置工具类上

1,330 阅读8分钟

前言

众所周知,TypeScript 官方提供了很多全局的内置工具类型,让我们不需要再重新造轮子并且可以直接使用,还可以通过这些全局工具类型来实现更为复杂的类型。

试想如果可以去试着理解和手写这些类型,也可以帮助我们打通任督二脉,更好的掌握和理解 TypeScript 中的各种类型,在项目中去自己造新轮子。


Partial<Type>

创建一个 Partial 类型,使传入的 Type 中所有属性都变成可选的。

type User = {
  name: string,
  age: number
};

type PartialType = Partial<User>;
// 相当于
type PartialType = {
  name?: string,
  age?: number
}

const user: PartialType = {
  age: 17
}

原理实现

通过 keyof 操作符获取已有对象类型的所有属性 key,给他们重新赋值类型,并将其设置为可选属性。

type Partial<T> = {
    [P in keyof T]?: T[P];
};

Required<Type>

创建一个 Required 类型,使传入的 Type 中所有属性都是必填项,是 Partial 类型的对立面。

type User = {
  name?: string,
  age?: number
};

type RequiredType = Required<User>;
// 相当于
type RequiredType = {
  name: string,
  age: number
}

const user: RequiredType = {
  age: 17
}
// 就会报错 Property 'name' is missing in type '{ age: number; }' but required in type 'Required<User>'.

原理实现

同样通过 keyof 操作符获取已有对象类型的所有属性 key,通过 TypeScript 提供的类型计算能力 - 修饰符给每个属性去掉 ? 修饰符,从可选状态转化为必填状态,并给他们重新赋值类型。

type Required<T> = {
    [P in keyof T]-?: T[P];
};

Readonly<Type>

创建一个 Readonly 类型,使传入的 Type 中所有属性都是只读的。

type User = {
  name: string,
  age: number
};

type ReadonlyType = Readonly<User>;

const user: ReadonlyType = {
	name: 'king',
  age: 17,
}

user.age = 18
// 就会报错 Cannot assign to 'age' because it is a read-only property.

原理实现

同样通过 keyof 操作符获取已有对象类型的所有属性 key,给他们重新赋值类型,并添加 readonly 关键字,就实现了只读操作。

type Readonly<T> = {
    readonly [P in keyof T]: T[P];
};

Pick<Type, Keys>

创建一个 Pick 类型,从传入到 Type 类型中,选择出部分属性 key 构成新的类型

type User = {
  name: string,
  age: number,
  gender: string
};

type PickType = Pick<User, 'name' | 'age'>
// 相当于
type PickType = {
  name: string,
  age: number,
}

const user: PickType = {
	name: 'king',
  age: 17,
  gender: 'male'
}
// 就会报错  Object literal may only specify known properties, and 'gender' does not exist in type 'Pick<User, "name" | "age">'.

原理实现

定义一个泛型 T 代表从 T 类型中去取 key 构成健,在 T 中找到 K 的属性并给他们重新赋值类型

type Pick<T, K extends keyof T> = {
    [P in K]: T[P];
};

Record<Keys, Type>

创建一个 Record 类型,将所有的属性集 Keys 的类型转化为 Type 类型构成新的类型,所以 Record 很适合来定义一个对象。

type User = {
  name: string,
  age: number
};

type serial = '1' | '2';

type RecordType = Record<serial, User>;

const user: RecordType = {
 '1': {
    name: 'kim',
  	age: 17
  },
 '2': {
    name: 'king',
  	age: 20
  }
}

原理实现

首先定义两个泛型 K 和 T,K 继承与 keyof any,也就是所有可以用来当作 key 的类型(包括 string、number、symbol),所以 K 就只能是 string、number、symbol 的类型之一,给他们重新赋值 T 类型。

type Record<K extends keyof any, T> = {
    [P in K]: T;
};

Exclude<Type, ExcludedUnion>

创建一个 Exclude 类型,从联合类型 Type 中排除 ExcludedUnion 类型构成新的联合类型。

type User = 'name' | 'age' | 'gender'

type ExcludeType = Exclude<User, 'gender'>;
// 相当于
type ExcludeType = 'name' | 'age'

原理实现

定义两个联合类型 T 和 U,如果 T extends U 成立,也就是说可以在 U 中的元素中找到 T 则需要排除,就返回 never 关键字(相当于不需要了),如果不成立就返回这个元素,就实现了排除功能。

type Exclude<T, U> = T extends U ? never : T;

Extract<Type, Union>

创建一个 Extract 类型,从 T 中提取出可赋值给 U 的那些类型成新的类型,与 Exclude 类型刚好是相反的。

type User = 'name' | 'age' | 'gender'

type ExcludeType = Exclude<User, 'name' | 'work'>;
// 相当于
type ExcludeType = 'name'

原理实现

定义两个联合类型 T 和 U,如果 T extends U 成立,说明在 T 中可以取出在 U 中存在的类型则返回,反之返回 never 去除掉。

type Exclude<T, U> = T extends U ? T : never;

Omit<Type, Keys>

创建一个 Omit 类型,移除接口或者类型 Type 中的 Keys 健构成新的类型。有点相似于 Exclude,但是 Exclude 操作的是联合类型,而 Omit 操作的是类型或者接口的键值对,是 Pick 类型的刚好是相反的。

type User = {
  name: string,
  age: number,
  gender: string
};

type OmitType = Omit<User, 'gender'>

const user: OmitType = {
  name: 'king',
  age: 17,
}

原理实现

定义一个类型 T 和需要省略的 key,key 可以是任何可以做健的类型,包括 string、number、symbol,使用 Exclude 类型去掉需要省略的 key,剩下的就是需要的类型,再使用 Pick 从 T 中找到这些类型构成新的类型。

type Omit<T, K extends keyof any> = {
  [P in keyof T as P extends K ? never : P]: T[P]; 
}

// 使用 Exclude 工具类 转换为:
type Omit<T, K extends keyof any> = {
  [P in Exclude<keyof T, K>]: T[P]; 
}

// 使用 Pick 工具类 转换为:
type Omit<T, K extends keyof any> = Pick<T, Exclude<keyof T, K>>;

NonNullable<Type>

创建一个 NonNullable 类型,从 T 中排除 null 和 undefined 构成新的类型

type NonNullableType = NonNullable<string | number | undefined | null>
// 相当于
type NonNullableType = string | number

原理实现

定义一个类型 T,如果 T 是 null 或者 undefined 其中的一个就去掉,反之就返回这个元素类型。

type NonNullable<T> = T extends null | undefined ? never : T;

Parameters<Type>

创建一个 Parameters 类型,获取函数类型 Type 的参数中使用的类型构造元组类型。

type ParametersType1 = Parameters<() => void>;
// 相当于
type ParametersType1 = []

type ParametersType2 = Parameters<(s: string) => string>;
// 相当于
type ParametersType2 = [s: string]

const add = (x: number, y: number): number => x + y;
type ParametersType3 = Parameters<typeof add>;
// 相当于
type ParametersType3 = [x: number, y: number]

// 还可以继续提取数组中的类型
type ParametersType4 = Parameters<typeof add>[0];
// 相当于
type ParametersType4 = number

原理实现

定义一个函数类型 T,如果 T 满足 T extends (...args: infer P) 这个函数,并且 args 参数使用 infer 关键字提取出了一个类型 P,就返回这个 P 类型,由于 args 参数提取的 P,所以 P 就是数组类型,若 T 不是函数类型则返回 never。

type Parameters<T extends (...args: any) => any> = T extends (...args: infer P) => any ? P : never;

ConstructorParameters<Type>

创建一个 ConstructorParameters 类型,获取构造函数类型 Type 的参数中使用的类型构成新的类型,与 Parameters 类型相似,但它适用于类构造函数。

class User {
  private name: string;
  private age: number;

  constructor(name: string, age: number) {
    this.name = name;
    this.age = age;
  }
}

type ConstructorParametersType = ConstructorParameters<typeof User>;
// 相当于
type ConstructorParametersType = [name: string, age: number]

原理实现

定义一个构造函数类型 T,如果满足 T extends abstract new (...args: infer P),就返回提取的这个 P 类型,P 类型同样是一个数组类型,若 T 不是构造函数类型则返回 never。

type ConstructorParameters<T extends abstract new (...args: any) => any> = T extends abstract new (...args: infer P) => any ? P : never;

ReturnType<Type>

创建一个 ReturnType 类型,获取函数类型 Type 的返回值类型构成新的类型。

type ReturnType1 = ReturnType<() => void>;
// 相当于
type ReturnType1 = void

type ReturnType2 = ReturnType<(s: string) => string>;
// 相当于
type ReturnType2 = string

const add = (x: number, y: number): number => x + y;
type ReturnType3 = ReturnType<typeof add>;
// 相当于
type ReturnType3 = number

原理实现

定义一个类型 T,使用 infer 关键字提取函数返回值的类型 R 方便之后使用,如果满足条件就返回 R。

type ReturnType<T extends (...args: any) => any> = T extends (...args: any) => infer R ? R : any;

InstanceType<Type>

创建一个 InstanceType 类型,获取构造函数类型的返回类型构成新的类型,与 ReturnType 类型相似,但它同样适用于类构造函数。

class User {
  private name: string;
  private age: number;

  constructor(name: string, age: number) {
    this.name = name;
    this.age = age;
  }
}

type InstanceTypeType = InstanceType<typeof User>;
// 相当于
type InstanceTypeType = User

原理实现

同样的定义一个类型 T,使用 infer 关键字提取构造函数返回值的类型 R,如果满足条件就返回 R。

type InstanceType<T extends abstract new (...args: any) => any> = T extends abstract new (...args: any) => infer R ? R : any;

ThisParameterType<Type>

创建一个 ThisParameterType 类型,用来提取函数 Type 中 this 的类型,若函数类型并没有此参数,则提取为 unknown 类型。

function toHex(this: Number) {
  return this.toString(16);
}

function numberToString(n: ThisParameterType<typeof toHex>) {
  return toHex.apply(n);
}

原理实现

定义一个类型 T,使用 infer 定义 this 的类型 U,如果计算成立返回 U,反之说明没有 this 返回 unknown。

type ThisParameterType<T> = T extends (this: infer U, ...args: any[]) => any ? U : unknown;

OmitThisParameter<Type>

创建一个 OmitThisParameter 类型,从函数类型 Type 中移除 this 参数。

function toHex(this: Number) {
  return this.toString(16);
}
 
const fiveToHex: OmitThisParameter<typeof toHex> = toHex.bind(5);
// 相当于                    
const fiveToHex = () => string;

原理实现

定义一个类型 T,如果 unknown extends ThisParameterType<T> 成立,也就是说明 ThisParameterType 类型返回的是 unknown,说明没有找到 this 参数,所以就不需要去移除 this,直接返回 T,反之返回一个不带 this 参数的类型。

type OmitThisParameter<T> = unknown extends ThisParameterType<T> ? T : T extends (...args: infer A) => infer R ? (...args: A) => R : T;

写在最后

具体的 Typescript 工具类源码可以查看相关的 github 地址。

不得不说,跟着手写了一遍这些工具类后,无论是对 Typescript 中一些 keyof 关键字、infer 关键字,还是各种类型的理解都在不断加深,确实也是一个帮助我们更好吸收掌握 Typescript 的方式。

如果你也同样对手写 Typescript 各种类型感兴趣,也可以去追求更高的挑战,这个拥有众多 stars 的 type-challenges 项目你值得拥有!