本文会和大家详细介绍 TypeScript 中的映射类型(Mapped Type),看完本文你将学到以下知识点:

  • 数学中的映射和 TS 中的映射类型的关系;
  • TS 中映射类型的应用;
  • TS 中映射类型修饰符的应用;

接下来会先从「数学中的映射」开始介绍。

本文使用到的 TypeScript 版本为 v4.6.2。

如果你对 TypeScript 还不熟悉,可以看下面几篇资料:

  1. 一份不可多得的 TS 学习指南(1.8W字)
  2. 了不起的 TypeScript 入门教程
  3. 【前端必备】用了 TS 映射类型,同事直呼内行!

    一、什么是映射?

    在学习 TypeScript 类型系统时,尽量多和数学中的集合类比学习,比如 TypeScript 中的联合类型,类似数学中的并集等。

在数学中,映射是指两个元素的集合之间元素相互对应的关系,比如下图:
image.png
(来源:https://baike.baidu.com/item/%E6%98%A0%E5%B0%84/20402621
可以将映射理解为函数,如上图,当我们需要将集合 A 的元素转换为集合 B 的元素,可以通过 f函数做映射,比如将集合 A 的元素 1对应到集合 B 中的元素 2
这样就能很好的实现映射过程的复用

二、TypeScript 中的映射类型是什么?

1. 概念介绍

TypeScript 中的映射类型和数学中的映射类似,能够将一个集合的元素转换为新集合的元素,只是 TypeScript 映射类型是将一个类型映射成另一个类型

在我们实际开发中,经常会需要一个类型的所有属性转换为可选类型,这时候你可以直接使用 TypeScript 中的 Partial工具类型:

  1. type User = {
  2. name: string;
  3. location: string;
  4. age: number;
  5. }
  6. type User2 = Partial<User>;
  7. /*
  8. User2 的类型:
  9. type User2 = {
  10. name?: string | undefined;
  11. location?: string | undefined;
  12. age?: number | undefined;
  13. }
  14. */

这样我们就实现了将 User类型映射成 User2类型,并且将 User类型中的所有属性转为可选类型。
image.png

2. 实现方法

TypeScript 映射类型的语法如下:

  1. type TypeName<Type> = {
  2. [Property in keyof Type]: boolean;
  3. };

我们既然可以通过 Partial工具类型非常简单的实现将指定类型的所有属性转换为可选类型,那其内容原理又是如何?
我们可以在编辑器中,将鼠标悬停在 Partial名称上面,可以看到编辑器提示如下:
image.png
拆解一下其中每个部分:

  • type Partial<T>:定义一个类型别名 Partial和泛型 T
  • keyof T:通过 keyof操作符获取泛型 T中所有 key,返回一个联合类型(如果不清楚什么是联合类型,可以理解为一个数组); ```typescript type User = { name: string; location: string; age: number; }

type KeyOfUser = keyof User; // “name” | “location” | “age”

  1. - `in`:类似 JS `for...in`中的 `in`,用来遍历目标类型的公开属性名;
  2. - `T[P]`:是个索引访问类型(也称查找类型),获取泛型 `T` `P`类型,类似 JS 中的访问对象的方式;
  3. - `?:`将类型值设置为可选类型;
  4. - `{ [P in keyof T] ?: T[P] | undefined}`:遍历 `keyof T`返回的联合类型,并定义用 `P`变量接收,其每次遍历返回的值为可选类型的 `T[P]`
  5. 这样就实现了 `Partial`工具类型,这种操作方法非常重要,是后面进行 TypeScript 类型体操的重要基础。
  6. > 关于类型体操的练习,有兴趣可以看看这篇文章:
  7. > 《这 30 TS 练习题,你能答对几道?》[https://juejin.cn/post/7009046640308781063](https://juejin.cn/post/7009046640308781063)
  8. <a name="dKStd"></a>
  9. ### 3. 映射类型示例
  10. 下面示例代码来自:[【前端必备】用了 TS 映射类型,同事直呼内行!](https://www.bilibili.com/video/BV1Wr4y1J7x3?spm_id_from=333.999.0.0)
  11. ```typescript
  12. type Item = { a: number, b: number, c: boolean}
  13. type T1 = { [P in 'x' | 'y']: number }
  14. // { x: number; y: number; }
  15. type T2 = { [P in 'x' | 'y']: P }
  16. // { x: "x"; y: "y"; }
  17. type T3 = { [P in 'a' | 'b']: Item[P] }
  18. // { a: number; b: number; }
  19. type T4 = { [P in keyof Item]: Item[P] }
  20. // { a: number; b: number; c: boolean; }

三、映射类型的应用

TypeScript 映射类型经常用来复用一些对类型的操作过程,比如 TypeScript 目前支持的 21 种工具类型,将我们常用的一些类型操作定义成这些工具类型,方便开发者复用这些类型。

所有已支持的工具类型可以看下官方文档: https://www.typescriptlang.org/docs/handbook/utility-types.html

下面我们挑几个常用的工具类型,看下其实现过程中是如何使用映射类型的。

在学习 TypeScript 过程中,推荐多在官方的 Playground 练习和学习: https://www.typescriptlang.org/zh/play

1. Required 必选属性

用来将类型的所有属性设置为必选属性
实现如下:

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

使用方式:

  1. type User = {
  2. name?: string;
  3. location?: string;
  4. age?: number;
  5. }
  6. type User2 = Required<User>;
  7. /*
  8. type User2 = {
  9. name: string;
  10. location: string;
  11. age: number;
  12. }
  13. */
  14. const user: User2 = {
  15. name: 'pingan8787',
  16. age: 18
  17. }
  18. /*
  19. 报错:
  20. Property 'location' is missing in type '{ name: string; age: number; }'
  21. but required in type 'Required<User>'.
  22. */

这边的 -?符号可以暂时理解为“将可选属性转换为必选属性”,下一节会详细介绍这些符号。

2. Readonly 只读属性

用来将所有属性的类型设置为只读类型,即不能重新分配类型。
实现如下:

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

使用方式:

  1. type User = {
  2. name?: string;
  3. location?: string;
  4. age?: number;
  5. }
  6. type User2 = Readonly<User>;
  7. /*
  8. type User2 = {
  9. readonly name?: string | undefined;
  10. readonly location?: string | undefined;
  11. readonly age?: number | undefined;
  12. }
  13. */
  14. const user: User2 = {
  15. name: 'pingan8787',
  16. age: 18
  17. }
  18. user.age = 20;
  19. /*
  20. 报错:
  21. Cannot assign to 'age' because it is a read-only property.
  22. */

3. Pick 选择指定属性

用来从指定类型中选择指定属性并返回
实现如下:

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

使用如下:

  1. type User = {
  2. name?: string;
  3. location?: string;
  4. age?: number;
  5. }
  6. type User2 = Pick<User, 'name' | 'age'>;
  7. /*
  8. type User2 = {
  9. name?: string | undefined;
  10. age?: number | undefined;
  11. }
  12. */
  13. const user1: User2 = {
  14. name: 'pingan8787',
  15. age: 18
  16. }
  17. const user2: User2 = {
  18. name: 'pingan8787',
  19. location: 'xiamen', // 报错
  20. age: 18
  21. }
  22. /*
  23. 报错
  24. Type '{ name: string; location: string; age: number; }' is not assignable to type 'User2'.
  25. Object literal may only specify known properties, and 'location' does not exist in type 'User2'.
  26. */

4. Omit 忽略指定属性

作用类似与 Pick工具类型相反,可以从指定类型中忽略指定的属性并返回。
实现如下:

  1. type Omit<T, K extends string | number | symbol> = {
  2. [P in Exclude<keyof T, K>]: T[P];
  3. }

使用方式:

  1. type User = {
  2. name?: string;
  3. location?: string;
  4. age?: number;
  5. }
  6. type User2 = Omit<User, 'name' | 'age'>;
  7. /*
  8. type User2 = {
  9. location?: string | undefined;
  10. }
  11. */
  12. const user1: User2 = {
  13. location: 'xiamen',
  14. }
  15. const user2: User2 = {
  16. name: 'pingan8787', // 报错
  17. location: 'xiamen'
  18. }
  19. /*
  20. 报错:
  21. Type '{ name: string; location: string; }' is not assignable to type 'User2'.
  22. Object literal may only specify known properties, and 'name' does not exist in type 'User2'.
  23. */

5. Exclude 从联合类型中排除指定类型

用来从指定的联合类型中排除指定类型
实现如下:

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

使用方式:

  1. type User = {
  2. name?: string;
  3. location?: string;
  4. age?: number;
  5. }
  6. type User2 = Exclude<keyof User, 'name'>;
  7. /*
  8. type User2 = "location" | "age"
  9. */
  10. const user1: User2 = 'age';
  11. const user2: User2 = 'location';
  12. const user3: User2 = 'name'; // 报错
  13. /*
  14. 报错:
  15. Type '"name"' is not assignable to type 'User2'.
  16. */

四、映射修饰符的应用

在自定义映射类型的时候,我们可以使用两个映射类型的修饰符来实现我们的需求:

  • readonly修饰符:将指定属性设置为只读类型
  • ?修饰符:将指定属性设置为可选类型

前面介绍 ReadonlyPartial工具类型的时候已经使用到:

  1. type Readonly<T> = {
  2. readonly [P in keyof T]: T[P];
  3. }
  4. type Partial<T> = {
  5. [P in keyof T]?: T[P] | undefined;
  6. }

当然,也可以对修饰符进行操作:

  • +添加修饰符(默认使用);
  • -删除修饰符;

比如:

  1. type Required<T> = {
  2. [P in keyof T]-?: T[P]; // 通过 - 删除 ? 修饰符
  3. };

也可以放在前面使用:

  1. type NoReadonly<T> = {
  2. -readonly [P in keyof T]: T[P]; // 通过 - 删除 readonly 修饰符
  3. }

总结一下常用的映射类型语法如下:

  1. { [ P in K ] : T}
  2. { [ P in K ] ?: T}
  3. { [ P in K ] -?: T}
  4. { readonly [ P in K ] : T}
  5. { readonly [ P in K ] ?: T}
  6. { -readonly [ P in K ] ?: T}

示例来自【前端必备】用了 TS 映射类型,同事直呼内行!

五、通过 as 实现键名重映射

从 TS 4.1 开始后,可以在映射类型中使用 as语句来实现键名重新映射:

  1. type MappedTypeWithNewProperties<T> = {
  2. [P in keyof T as NewKeyType]: T[P]
  3. }

[P in keyof T as NewKeyType]: T[P]这段为例,as 语句的阅读顺序如下:

  1. 先通过 keyof T获取 T类型的所有属性名称的联合类型;
  2. 然后通过 in遍历该联合类型,赋值给变量 P
  3. 然后通过 asP重映射成 NewKeyType类型;
  4. 最后设置其类型为 T[P]

image.png
举个例子,需要将对象中的属性全部转换为方法类型,比如:

  1. // 使用模版字面量类型
  2. type Getters<T> = {
  3. [P in keyof T as `get${Capitalize<string & P>}`]: () => T[P]
  4. };

下一节【TS 编译器内部实现的类型】会详细介绍 Capitalize 类型

由于 keyof T 返回的是联合类型,可能包含 string/number/symbol类型,而Capitalize工具类型只能接收 string类型,所以这边需要用过 <string & P>交叉类型过滤非 string类型的键。

也可以使用条件类型返回一个 never从而过滤掉某些属性:

  1. // 移除 kind 属性
  2. type RemoveKindField<T> = {
  3. [P in keyof T as Exclude<P, "kind">]: T[P];
  4. };
  5. // type Exclude <T, U> = T extends U ? never : T;
  6. interface Circle {
  7. kind: "circle";
  8. radius: number;
  9. }
  10. type KindlessCircle = RemoveKindField<Circle>;

还有几个示例方便理解:

  1. type Omit1<T, U> = Pick<T, Exclude<keyof T, U>>
  2. // 展开 Pick 得到如下
  3. type Omit2<T, U extends keyof T> = {
  4. [K in Exclude<keyof T, U>] : T[K]
  5. }
  6. // 不能这样展开 Exclude
  7. type Omit3<T, U> = {
  8. [key in T extends U ? never : T]: T[key]
  9. }
  10. // 可以使用 as 展开 Exclude 得到如下
  11. type Omit3<T, U extends keyof T> = {
  12. [K in keyof T as K extends U ? never : K] : T[K]
  13. }

六、补充

1. TS 编译器内部实现的类型

文档地址:https://www.typescriptlang.org/docs/handbook/2/template-literal-types.html

TypeScript 为了帮助开发者对字符串进行操作,提供了一组可用于字符串操作的类型。这些类型内置于编译器以提高性能,在 TypeScript 包含的 lib.es5.d.ts 文件中找不到具体的定义:

  1. type Uppercase<S extends string> = intrinsic;
  2. type Lowercase<S extends string> = intrinsic;
  3. type Capitalize<S extends string> = intrinsic;
  4. type Uncapitalize<S extends string> = intrinsic;

目前支持的类型包括:

  • Uppercase<StringType>

用来将字符串中的每个字符转换为大写。

  1. type B1 = Uppercase<'pingan8787'>;
  2. // PINGAN8787
  • Lowercase<StringType>

用来将字符串中的每个字符转换为小写。

  1. type B2 = Lowercase<'PINGAN8787'>;
  2. // pingan8787
  • Capitalize<StringType>

用来将字符串中的首个字符转换为大写。

  1. type B3 = Capitalize<'pingan8787'>;
  2. // Pingan8787
  • Uncapitalize<StringType>

用来将字符串中的首个字符转换为小写。

  1. type B4 = Uncapitalize<'PINGAN8787'>;
  2. // pINGAN8787

2. 映射类型支持元组和数组类型

文档地址:https://www.typescriptlang.org/docs/handbook/release-notes/typescript-3-1.html

在 TypeScript 3.1 版本之后,映射类型也支持了元组和数组类型。

  1. type MapToPromise<T> = { [K in keyof T]: Promise<T[K]> };
  2. type Coordinate = [number, number]; // 元组类型
  3. type PromiseCoordinate = MapToPromise<Coordinate>; // [Promise<number>, Promise<number>]

MapToPromise接收类型 T,当 T 为Coordinate这类元组时,只有 number的类型会被转换。在 [number, number]中,有 2 个以数字命名的属性: 01。当给定的是这样类型的元组时,MapToPromise将创建一个新的元组,其中01属性是原始类型的 Promise。因此返回 [Promise<number>, Promise<number>]类型。
可以再看下:

  1. type Simplify<T> = {
  2. [P in keyof T]: T[P]
  3. }
  4. type A1 = Simplify<keyof [number, number]>;
  5. /*
  6. 元组类型的 keyof
  7. type A1 = number | typeof Symbol.iterator | typeof Symbol.unscopables |
  8. "0" | "1" | "length" | "toString" | "toLocaleString" | "pop" | "push" |
  9. "concat" | "join" | "reverse" | ... 21 more ... | "includes"
  10. */
  11. type A2 = Simplify<keyof number[]>
  12. /*
  13. 数组类型的 keyof
  14. type A2 = number | typeof Symbol.iterator | typeof Symbol.unscopables |
  15. "length" | "toString" | "toLocaleString" | "pop" | "push" | "concat" |
  16. "join" | "reverse" | "shift" | ... 20 more ... | "includes"
  17. */

七、总结

本文从数学中的映射作为切入点,详细介绍 TypeScript 映射类型(Mapped Type)并介绍映射类型的应用和修饰符的应用。
在学习 TypeScript 类型系统时,尽量多和数学中的集合类比学习,比如 TypeScript 中的联合类型,类似数学中的并集等。
学好映射类型,是接下来做类型体操中非常重要的基础~~

参考资料

  1. TypeScript 文档-映射类型:https://www.typescriptlang.org/docs/handbook/2/mapped-types.html
  2. TypeScript 工具类型: https://www.typescriptlang.org/docs/handbook/utility-types.html