TypeScript Utility Types 解读

219 阅读12分钟

TypeScript 提供了一些实用类型工具方法(Utility Types),以方便进行常见的类型转换。这些类型工具方法在全局范围内都能使用。

  • Partial<Type>
  • Readonly<Type>
  • Record<Keys,Type>
  • Pick<Type, Keys>
  • Required<Type>
  • Exclude<Type, ExcludedUnion>
  • Extract<Type, Union>
  • NonNullable<Type>
  • Omit<Type, Keys>
  • Parameters<Type>
  • ConstructorParameters<Type>
  • ReturnType<Type>
  • InstanceType<Type>
  • ThisParameterType<Type>
  • OmitThisParameter<Type>
  • ThisType<Type>

singsong: 这些方法在 TypeScript 中,也被称为 Mapped Types

在介绍这些方法之前,首先得先掌握一些基础概念,以便更好地理解这些方法源码。

相关基础知识介绍

  • keyof

    keyof 操作符,也称为索引类型查询符。是在 TypeScript 2.1 版本引入的,该操作符可以用于获取某种类型的所有键,其返回类型是所有键联合类型。其行为与 Object.keys() 方法类似。

    interface Person {
      name: string;
      age: number;
      location: string;
    }
    
    type K1 = keyof Person; // "name" | "age" | "location"
    type K2 = keyof Person[]; // "length" | "push" | "pop" | "concat" | ...
    type K3 = keyof { [x: string]: Person }; // string
    
  • 查询类型(Lookup Types

    语法上,类似于元素访问。而这里是类型查询

    type P1 = Person['name']; // string
    type P2 = Person['name' | 'age']; // string | number
    type P3 = string['charAt']; // (pos: number) => string
    type P4 = string[]['push']; // (...items: string[]) => number
    type P5 = string[][0]; // string
    
  • in 操作符,表示如果指定属性在指定对象中,则会返回 true。 在 Tyepscript 中常用于 Mapped Types 遍历联合类型所有键。

    interface Person {
      name: string;
      age: number;
    }
    type Partial<T> = {
      [P in keyof T]?: T[P]; //等效于 P in ("name" | "age")
    };
    type PersonPartial = Partial<Person>; // 等同于 { name?: string;  age?: number; }
    

    还可用于类型保护(Type Guards

    interface A {
      x: number;
    }
    interface B {
      y: string;
    }
    
    let q: A | B = ...;
    if ('x' in q) {
    // q: A
    } else {
    // q: B
    }
    
  • 泛型约束

    在使用泛型时,由于无法确定类型,随意地操作其属性或方法,可能会导致编译报错:

    function loggingIdentity<T>(arg: T): T {
      console.log(arg.length);
      return arg;
    }
    
    // index.ts(2,19): error TS2339: Property 'length' does not exist on type 'T'.
    

    上述实例中,泛型 T 不一定包含属性 length,所以编译时报错。 这里可以对泛型 T 进行约束,泛型 T 必须包含 length 属性才能允许传入。这就是泛型约束。

    interface Lengthwise {
      length: number;
    }
    
    function loggingIdentity<T extends Lengthwise>(arg: T): T {
      console.log(arg.length);
      return arg;
    }
    

    上述实例中,extends Lengthwise 对泛型 T 进行约束,T 必须符合接口 Lengthwise 形状(即包含 length)属性。

  • never 类型

    表示永远不会发生的类型。主要有如下两种场景:

    • 绝不会有返回值的函数

      function infiniteLoop(): never {
        while (true) {}
      }
      
    • 总是抛出错误的函数

      function error(message: string): never {
        throw new Error(message);
      }
      

    ❗️ 注意:never 只能赋值给另一个 never,不能赋值给其他类型

  • 条件类型(Conditional Types

    SomeType extends OtherType ? TrueType : FalseType;
    

    SomeType extends OtherType 中的 SomeType 被赋值,就取 TrueType,否则取 FalseType

    分发条件类型(Distributive conditional types)

    如果 SomeType 是一个 naked type parameter(裸类型参数),则该条件类型为分发条件类型。

    singsong: 什么是 naked type parameter(裸类型参数)它表示没有被包裹的类型,如:Array、[T]、Promise 等都不是 naked type parameter(裸类型参数)

    分发条件类型 主要用于拆分 extends 左边部分的联合类型。如

    type DCType<T> = T extends U ? X : Y;
    // 如果 T 取值 A | B | C
    type DCType<A|B|C> = (A extends U ? X : Y) | (B extends U ? X : Y) | (C extends U ? X : Y);
    
  • infer

    表示在 extends 条件类型 中待推断的类型变量。

    type ParamType<T> = T extends (param: infer P) => any ? P : T;
    

    如果 T 能赋值给 (param: infer P) => any,则结果是 (param: infer P) => any 类型中的参数 P,否则返回为 T。

Partial

将某个类型 Type 中的属性全部转换为可选属性(?

源码

//github.com/microsoft/TypeScript/blob/master/lib/lib.es5.d.ts#L1459

/**
 * Make all properties in T optional
 */
https: type Partial<T> = {
  [P in keyof T]?: T[P];
};

源码解读:首先通过 keyof 获取类型变量 T 的所有键的联合类型 keyof T,再使用 in 操作符遍历,将所有键变为可选 [p]?,并通过 Lookup Types 获取对应的类型值 T[P] 赋值给 [P]?: T[P]

实例演示

interface Todo {
  title: string;
  description: string;
}

type TodoPartial = Partial<Todo>;

// 等同于

type TodoPartial = {
  title?: string;
  description?: string;
};
  1. keyof Todo 返回 "title" | "description";

  2. [P in keyof T]?: T[P] 等同于 [P in "title" | "description"]?: Todo[P]; 类似于

    for(let P in Todo){
      TodoPartial[P]?: Todo[P]
    }
    
  3. TodoPartial 等于 {title?: string; description?: string;}

Readonly

将某个类型 Type 中的属性全部转换为只读属性(readonly)。

源码

// https://github.com/microsoft/TypeScript/blob/master/lib/lib.es5.d.ts#L1473

/**
 * Make all properties in T readonly
 */
type Readonly<T> = {
  readonly [P in keyof T]: T[P];
};

源码解读:首先通过 keyof 获取类型变量 T 的所有键的联合类型 keyof T,再使用 in 操作符遍历,将所有键变为只读 readonly [p],并通过 Lookup Types 获取对应的类型值 T[P] 赋值给 readonly [P]: T[P]

实例演示

interface Todo {
  title: string;
  description: string;
}

type TodoReadonly = Readonly<Todo>;

// 等同于

type TodoReadonly = {
  readonly title: string;
  readonly description: string;
};
  1. keyof Todo 返回 "title" | "description";

  2. readonly [P in keyof T]: T[P] 等同于 readonly [P in "title" | "description"]: Todo[P]; 类似于

    for(let P in Todo){
      readonly TodoReadonly[P]: Todo[P]
    }
    
  3. TodoReadonly 等于

     {
       readonly title: string;
       readonly description: string;
     }
    

Record<Keys,Type>

Keys 所有的键转为了 Type 类型

源码

// https://github.com/microsoft/TypeScript/blob/master/lib/lib.es5.d.ts#L1487

/**
 * Construct a type with a set of properties K of type T
 */
type Record<K extends keyof any, T> = {
  [P in K]: T;
};

源码解读:这里 keyof any 等同于 string | number | symbol,即索引类型所有合法取值。 K extends keyof any 等同于 K extends string | number | symbol,对 K 进行 泛型约束,即 K 只能是 string | number | symbol 其中任何一个。[P in K]: T; 使用 in 操作符遍历,将所有键都赋值 T

实例演示

interface Todo {
  title: string;
  description: string;
}

type Pages = 'home' | 'about';

type TodoRecord = Record<Pages, Todo>;

// 等同于

type TodoRecord = {
  home: {
    title: string;
    description: string;
  };
  about: {
    title: string;
    description: string;
  };
};
  1. Pages 只能取值 string | number | symbol;

  2. [P in Pages]: Todo 等同于 [P in "home" | "about"]: Todo; 类似于

    for(let P in Pages){
      TodoRecord[P]: Todo
    }
    
  3. TodoRecord 等于

    home: {
      title: string;
      description: string;
    }
    about: {
      title: string;
      description: string;
    }
    

Pick<Type, Keys>

从某个类型 Type 中,提取 Keys 中指定的所有键的子类型。

源码

// https://github.com/microsoft/TypeScript/blob/master/lib/lib.es5.d.ts#L1480

/**
 * From T, pick a set of properties whose keys are in the union K
 */
type Pick<T, K extends keyof T> = {
  [P in K]: T[P];
};

源码解读:这里 K extends keyof T 主要对 K 进行 泛型约束,取值只能是 keyof T[P in K]: T; 使用 in 操作符遍历,并通过 Lookup Types 获取对应的类型值 T[P] 赋值给 [P]: T[P]

实例演示

interface Todo {
  id: number;
  title: string;
  description: string;
}

type Keys = 'title' | 'description';

type TodoPick = Pick<Todo, Keys>;

// 等同于

type TodoPick = {
  title: string;
  description: string;
};
  1. Keys 只能取值 "id" | title" | "description";

  2. [P in Keys]: Todo[P] 等同于 [P in "title" | "description"]: Todo[P]; 类似于

    for(let P in Keys){
      TodoPick[P]: Todo[P]
    }
    
  3. TodoPick 等于

    {
      title: string;
      description: string;
    }
    

Required<Type>

将某个类型 Type 中的属性全部转换为必选属性。

源码

// https://github.com/microsoft/TypeScript/blob/master/lib/lib.es5.d.ts#L1466

/**
 * Make all properties in T required
 */
type Required<T> = {
  [P in keyof T]-?: T[P];
};

源码解读:首先通过 keyof 获取类型变量 T 的所有键的联合类型 keyof T,再使用 in 操作符遍历,将所有键变为”必选[p]-?,并通过 Lookup Types 获取对应的类型值 T[P] 赋值给 [P]-?: T[P]

singsong: 这里的 "-?" 表示什么 🤔🤔。它表示删除可选符号 ?,即 必选

实例演示

interface Todo {
  title?: string;
  description?: string;
}

type TodoRequired = Required<Todo>;

// 等同于

type TodoRequired = {
  title: string;
  description: string;
};
  1. keyof Todo 返回 "title" | "description";

  2. [P in keyof T]-?: T[P] 等同于 [P in "title" | "description"]-?: Todo[P]; 类似于

    for(let P in Todo){
      TodoRequired[P]-?: Todo[P]
    }
    
  3. TodoRequired 等于

    {
      title: string;
      description: string;
    }
    

Exclude<Type, ExcludedUnion>

Type 类型中属于另一个的类型 ExcludedUnion 移除掉

源码

// https://github.com/microsoft/TypeScript/blob/master/lib/lib.es5.d.ts#L1494

/**
 * Exclude from T those types that are assignable to U
 */
type Exclude<T, U> = T extends U ? never : T;

源码解读:如果 T 被赋值(T 符合 U 约束),就取 never(移除),否则取 T

实例演示

type T = Exclude<'a' | 'b' | 'c', 'a' | 'b'>;

// 等同于

type T = 'a' extends 'a' | 'b' ? never : 'a' | 'b' extends 'a' | 'b' ? never : 'b' | 'c' extends 'a' | 'b' ? never : 'c'; // 分发条件类型

// 等同于

type T = 'c';

singsong: 当条件类型用于泛型时,如果 T 是个联合类型,会进行分发条件类型(Distributive Conditional Types

Extract<Type, Union>

Type 类型中属于另一个的类型 Union 提取出构建成新的子类型。

源码

// https://github.com/microsoft/TypeScript/blob/master/lib/lib.es5.d.ts#L1499

/**
 * Extract from T those types that are assignable to U
 */
type Extract<T, U> = T extends U ? T : never;

源码解读:如果 T 被赋值(T 符合 U 约束),就取 T(提取),否则取 never(移除)。

实例演示

type T = Extract<'a' | 'b' | 'c', 'a' | 'b'>;

// 等同于

type T = 'a' extends 'a' | 'b' ? 'a' : never | 'b' extends 'a' | 'b' ? 'b' : never | 'c' extends 'a' | 'b' ? 'c' : never; // 分发条件类型

// 等同于

type T = 'a' | 'b';

singsong: 当条件类型用于泛型时,如果 T 是个联合类型,会进行分发条件类型(Distributive Conditional Types

NonNullable<Type>

Type 类型中属于 null | undefined 移除。

源码

// https://github.com/microsoft/TypeScript/blob/master/lib/lib.es5.d.ts#L1509

/**
 * Exclude null and undefined from T
 */
type NonNullable<T> = T extends null | undefined ? never : T;

源码解读:如果 T 被赋值(T 符合 null | undefined 约束),就取 never(移除),否则取 T

实例演示

type T = NonNullable<string | number | undefined>;

// 等同于

type T = string extends null | undefined
  ? never
  : string | number extends null | undefined
  ? never
  : number | undefined extends null | undefined
  ? never
  : undefined; // 分发条件类型

// 等同于

type T = string | number;

Omit<Type, Keys>

从某个类型 Type 中,提取 Keys 中指定的所有键的子类型。

源码

// https://github.com/microsoft/TypeScript/blob/master/lib/lib.es5.d.ts#L1504

/**
 * Construct a type with the properties of T except for those in type K.
 */
type Omit<T, K extends keyof any> = Pick<T, Exclude<keyof T, K>>;

源码解读:这里 keyof any 等同于 string | number | symbol,即索引类型所有合法取值。 K extends keyof any 等同于 K extends string | number | symbol,对 K 进行 泛型约束,即 K 只能是 string | number | symbol 其中任何一个。Exclude<keyof T, K> 使用 Exclude 方法将 KT 移除。Pick<T, Exclude<keyof T, K>> 使用 Pick 方法从 T 中提取 Exclude<keyof T, K>。关于 ExcludePick 请参考上述讲解。

实例演示

interface Todo {
  id: number;
  title: string;
  description: string;
}

type Keys = 'title' | 'description';

type TodoOmit = Omit<Todo, Keys>;

// 等同于

type TodoOmit = {
  id: number;
};
  1. 使用 Exclude 方法,移除 KeysExclude<Todo, 'title' | 'description'>

  2. 使用 Pick 方法,从 Todo 中提取 Exclude<Todo, 'title' | 'description'>Pick<Todo, Exclude<Todo, 'title' | 'description'>>

  3. TodoOmit 等于

    {
      id: number;
    }
    

Parameters<Type>

获取 Type 函数类型的参数类型。

源码

// https://github.com/microsoft/TypeScript/blob/master/lib/lib.es5.d.ts#L1514

/**
 * Obtain the parameters of a function type in a tuple
 */
type Parameters<T extends (...args: any) => any> = T extends (...args: infer P) => any ? P : never;

源码解读

T extends (...args: any) => any 这里主要对 T 进行泛型约束,只能取函数类型。

type paramType = Parameters<number>;

// ❌ Type 'number' does not satisfy the constraint '(...args: any) => any'.

T extends (...args: infer P) => any ? P : never 表示 T 如果符合 (...args: infer P) => any 约束,就返回 P,否则 neverinfer P 用于表示 T 函数参数类型。因为不知道函数参数类型,需要使用 infer 声明一个新的类型变量 P 表示参数类型。

singsong: infer 只能用于 extends 的条件语句中。

实例演示

type add = (arg: { a: number; b: string }) => number;
type addParamType = Parameters<add>;

// 等同于

type addParamType = [
  arg: {
    a: number;
    b: string;
  }
];

关于 infer 更多

ConstructorParameters<Type>

获取 Type 构造函数类型的参数类型。

源码

// https://github.com/microsoft/TypeScript/blob/master/lib/lib.es5.d.ts#L1519

/**
 * Obtain the parameters of a constructor function type in a tuple
 */
type ConstructorParameters<T extends new (...args: any) => any> = T extends new (...args: infer P) => any ? P : never;

源码解读

T extends new (...args: any) => any 这里主要对 T 进行泛型约束,只能取构造函数类型。

type add = (arg: { a: number; b: string }) => number;
type constructorParametersType = ConstructorParameters<add>;

// ❌ Type '(arg: { a: number; b: string; }) => any' does not satisfy the constraint 'new (...args: any) => any'.
// ❌ Type '(arg: { a: number; b: string; }) => any' provides no match for the signature 'new (...args: any): any'.

T extends new (...args: infer P) => any ? P : never 表示 T 如果符合 new (...args: infer P) => any 约束,就返回 P,否则 neverinfer P 用于表示 T 构造函数参数类型。因为不知道构造函数参数类型,需要使用 infer 声明一个新的类型变量 P 表示参数类型。

singsong: infer 只能用于 extends 的条件语句中。

实例演示

type Add = new (arg: { a: number; b: string }) => number;
type constructorParametersType = ConstructorParameters<Add>;

// 等同于

type constructorParametersType = [
  arg: {
    a: number;
    b: string;
  }
];

InstanceType<Type>

获取 Type 构造函数类型的返回类型。

源码

// https://github.com/microsoft/TypeScript/blob/master/lib/lib.es5.d.ts#L1524

/**
 * Obtain the return type of a constructor function type
 */
type InstanceType<T extends new (...args: any) => any> = T extends new (...args: any) => infer R ? R : any;

源码解读

T extends new (...args: any) => any 这里主要对 T 进行泛型约束,只能取构造函数类型。

type add = (arg: { a: number; b: string }) => number;
type instanceType = InstanceType<add>;

// ❌ Type '(arg: { a: number; b: string; }) => any' does not satisfy the constraint 'new (...args: any) => any'.
// ❌ Type '(arg: { a: number; b: string; }) => any' provides no match for the signature 'new (...args: any): any'.

T extends new (...args: any) => infer R ? R : any 表示 T 如果符合 new (...args: any) => infer R 约束,就返回 R,否则 anyinfer R 用于表示 T 构造函数类型的返回类型。因为不知道构造函数返回类型,需要使用 infer 声明一个新的类型变量 R 表示返回类型。

singsong: infer 只能用于 extends 的条件语句中。

实例演示

type Add = new (arg: { a: number; b: string }) => number;
type instanceType = InstanceType<Add>;

// 等同于

type instanceType = number;

ReturnType<Type>

获取 Type 函数类型的返回类型。

源码

// https://github.com/microsoft/TypeScript/blob/master/lib/lib.es5.d.ts#L1514

/**
 * Obtain the return type of a function type
 */
type ReturnType<T extends (...args: any) => any> = T extends (...args: any) => infer R ? R : any;

源码解读

T extends (...args: any) => any 这里主要对 T 进行泛型约束,只能取函数类型。

type returnType = ReturnType<number>;

// ❌ Type 'number' does not satisfy the constraint '(...args: any) => any'.

T extends (...args: any) => infer R ? R : any 表示 T 如果符合 (...args: any) => infer R 约束,就返回 R,否则 anyinfer R 用于表示 T 函数类型的返回类型。因为不知道函数返回类型,需要使用 infer 声明一个新的类型变量 R 表示返回类型。

singsong: infer 只能用于 extends 的条件语句中。

实例演示

type fn = () => { a: number; b: string };
type returnType = ReturnType<fn>;

// 等同于

type returnType = {
  a: number;
  b: string;
};

ThisParameterType<Type>

获取 Type 函数类型的 this 参数类型,如果函数类型没有 this 参数,则返回 unknown

源码

// https://github.com/microsoft/TypeScript/blob/master/lib/lib.es5.d.ts#L321

/**
 * Extracts the type of the 'this' parameter of a function type, or 'unknown' if the function type has no 'this' parameter.
 */
type ThisParameterType<T> = T extends (this: infer U, ...args: any[]) => any ? U : unknown;

源码解读

T extends (this: infer U, ...args: any[]) => any ? U : unknown 表示 T 如果符合 (this: infer U, ...args: any[]) => any 约束,就返回 U,否则 unknowninfer U 用于表示 U 函数类型的 this 参数类型。因为不知道函数类型的 this 参数类型,需要使用 infer 声明一个新的类型变量 U 表示返回类型。

singsong: infer 只能用于 extends 的条件语句中。

实例演示

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

type thisParameterType = ThisParameterType<typeof toHex>; // typeof toHex 用于获取 `toHex` 函数类型

// 等同于

type thisParameterType = Number;

OmitThisParameter<Type>

删除 Type 函数类型的 this 参数类型。

源码

// https://github.com/microsoft/TypeScript/blob/master/lib/lib.es5.d.ts#L326

/**
 * Removes the 'this' parameter from a function type.
 */
type OmitThisParameter<T> = unknown extends ThisParameterType<T>
  ? T
  : T extends (...args: infer A) => infer R
  ? (...args: A) => R
  : T;

源码解读

unknown extends ThisParameterType<T> ? T : T extends (...args: infer A) => infer R ? (...args: A) => R : T 表示 T 函数类型没有 this 参数类型(unknown extends ThisParameterType<T>true),则直接返回 T。否则 T extends (...args: infer A) => infer R ? (...args: A) => R : T 表示如果 T 是个函数类型,则创建一个新的没有 this 参数类型的函数类型((...args: A) => R);否则直接返回 T

singsong: infer 只能用于 extends 的条件语句中。

实例演示

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

type omitThisParameterType = OmitThisParameter<typeof toHex>; // typeof toHex 用于获取 `toHex` 函数类型

// 等同于

type omitThisParameterType = () => string;

ThisType<Type>

控制 this 类型,该方法只在 --noImplicitThis 选项开启条件下才有效。

源码

// https://github.com/microsoft/TypeScript/blob/master/lib/lib.es5.d.ts#L1534

/**
 * Marker for contextual 'this' type
 */
interface ThisType<T> {}

源码解读

一个空接口,它不返回任何转换过的类型,而是作为对象字面量上下文 this 标识。如果要使用这个类型,需要启用配置 -noImplicitThis

实例演示

type Person = {
  name: string;
  age: number;
};

type Student = {
  context: ThisType<Person> & { id: number };
};

// 等同于

type Student = {
  context: {
    name: string;
    age: number;
  } & { id: number };
};

总结

本文以实例形式详细地解读了 TypeScript 中 Utility Types。只有了解 Utility Types 实现细节,才能愉快地使用和定制 Utility Types 。。。。😁 😁 😁