用了 TS 映射类型,同事直呼内行!

24,871 阅读4分钟

不想看文字,那就直接来看视频吧www.bilibili.com/video/BV1Wr…

你用过 Partial、Required、Pick 和 Readonly 这些工具类型吗?

你知道它们内部是如何工作的吗?

如果你想彻底掌握它们且实现自己的工具类型,那么本文介绍的内容千万不要错过。

在日常工作中,用户注册是一个很常见的场景。这里我们可以使用 TS 定义一个 User 类型,在该类型中所有的键都是必填的。

type User = {
  name: string; // 姓名
  password: string; // 密码
  address: string; // 地址
  phone: string; // 联系电话
};

通常情况下,对于已注册的用户,我们是允许用户只修改部分用户信息。这时我们就可以定义一个新的 UserPartial 类型,表示用于更新的用户对象的类型,在该类型中所有的键都是可选的。

type UserPartial = {
  name?: string; // 姓名
  password?: string; // 密码
  address?: string; // 地址
  phone?: string; // 联系电话
};

而对于查看用户信息的场景,我们希望该用户对象所对应的对象类型中所有的键都是只读。针对这种需求,我们可以定义 ReadonlyUser 类型。

type ReadonlyUser = {
  readonly name: string; // 姓名
  readonly password: string; // 密码
  readonly address: string; // 地址
  readonly phone: string; // 联系电话
};

回顾前面已定义的与用户相关的 3 种类型,你会发现它们中含有很多重复的代码。

那么如何减少以上类型中的重复代码呢?

答案是可以使用映射类型,它是一种泛型类型,可用于把原有的对象类型映射成新的对象类型

映射类型的语法如下:

{ [ P in K ] : T }

其中 P in K 类似于 JavaScript 中的 for...in 语句,用于遍历 K 类型中的所有类型,而 T 类型变量用于表示 TS 中的任意类型。

在映射的过程中,你还可以使用 readonly 和问号这两个额外的修饰符。通过添加加号和减号前缀,来增加和移除对应的修饰符。如果没有添加任何前缀的话,默认是使用加号。

现在我们就可以总结出常见的映射类型语法:

{ [ P in K ] : T }
{ [ P in K ] ?: T }
{ [ P in K ] -?: T }
{ readonly [ P in K ] : T }
{ readonly [ P in K ] ?: T }
{ -readonly [ P in K ] ?: T }

介绍完映射类型的语法,我们来看一些具体的例子:

type Item = { a: string; b: number; c: boolean };

type T1 = { [P in "x" | "y"]: number }; // { x: number, y: number }
type T2 = { [P in "x" | "y"]: P }; // { x: "x", y: "y" }
type T3 = { [P in "a" | "b"]: Item[P] }; // { a: string, b: number }
type T4 = { [P in keyof Item]: Item[P] }; // { a: string, b: number, c: boolean }

下面我们来看一下如何利用映射类型来重新定义 UserPartial 类型:

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

type UserPartial = MyPartial<User>;

在以上代码中,我们定义了 MyPartial 映射类型,然后利用该类型把 User 类型映射成 UserPartial 类型。其中 keyof 操作符用于获取某种类型中的所有键,其返回类型是联合类型。而类型变量 P 会随着每次遍历改变成不同的类型,T[P] 该语法类似于属性访问的语法,用于获取对象类型某个属性对应值的类型。

下面我们来演示一下 MyPartial 映射类型的完整执行流程,如果不清楚的话,可以多看几遍加深对 TS 映射类型的理解。

TypeScript 4.1 版本允许我们使用 as 子句对映射类型中的键进行重新映射。它的语法如下:

type MappedTypeWithNewKeys<T> = {
    [K in keyof T as NewKeyType]: T[K]
    //            ^^^^^^^^^^^^^
    //            这是新的语法!
}

其中 NewKeyType 的类型必须是 string | number | symbol 联合类型的子类型。使用 as 子句,我们可以定义一个 Getters 工具类型,用于为对象类型生成对应的 Getter 类型:

type Getters<T> = {
  [K in keyof T as `get${Capitalize<string & K>}`]: () => T[K]
};

interface Person {
    name: string;
    age: number;
    location: string;
}

type LazyPerson = Getters<Person>;
// {
//   getName: () => string;
//   getAge: () => number;
//   getLocation: () => string;
// }

在以上代码中,因为 keyof T 返回的类型可能会包含 symbol 类型,而 Capitalize 工具类型要求处理的类型需要是 string 类型的子类型,所以需要通过交叉运算符进行类型过滤。

此外,在对键进行重新映射的过程中,我们可以通过返回 never 类型对键进行过滤:

// Remove the 'kind' property
type RemoveKindField<T> = {
    [K in keyof T as Exclude<K, "kind">]: T[K]
};

interface Circle {
    kind: "circle";
    radius: number;
}

type KindlessCircle = RemoveKindField<Circle>;
//   type KindlessCircle = {
//       radius: number;
//   };

看完本文之后,相信你已经了解映射类型的作用了,也知道 TS 内部一些工具类型是如何实现的。你喜欢以这种形式学 TS 么?喜欢的话,记得点赞与收藏。

TypeScript 泛型中的 K、T、V 等到底是个啥?

轻松学 TypeScript 视频教程已更新了 7 集,本专题将会以形象生动的动画,带你一起学习 TypeScript 核心知识点,感兴趣的小伙伴一起学起来呀!