【译】精通TypeScript系列 - 像大佬一样使用映射类型
声明:本文是翻译文章,原文为Using TypeScript Mapped Types Like a Pro,作者是Bytefer
欢迎来到精通TypeScript系列文章,我会在这一系列文章里面以动画和图片的形式给大家介绍TypeScript中一些核心的知识以及技术。本篇文章将会为大家深入介绍TypeScript的映射类型,让大家可以从TypeScript小白晋升为大佬!话不多说,现在让我们开始学习吧!
首先问你一个问题:你是否使用过Partial,Required,Readonly和Pick这些工具类型(Utility Types)?
如果用过,那你知道它们内部是如何实现的吗?不知道也没关系,因为本篇文章会帮你彻底搞懂这些工具类型的内部实现并且创造属于自己的工具类型!
在我们日常工作中,用户注册是一个很通用的场景,现在我们来为这个需求定义一个User类型,这个用户类型的所有属性都是必须的,具体定义如下:
type User {
name: string;
password: string;
address: string;
phone: string;
}
用户完成注册后,可能会需要更新自己的用户信息,为了实现这个需求我们可以定义一个新的类型叫UserPartial, 这个类型只允许用户更改自己的某些信息例如名字或者地址等,虽然它包含的属性和用户类型是一样的,不过它的所有属性(keys)都是可选的:
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;
}
为了实现三个需求,我们定义了三个具有相同keys的用户类,它们唯一的区别是key的属性值不一样,这就意味着它们三个有很多重复的代码,维护起来会相当麻烦。
所以我们可以去掉上面这些类型里面的重复代码吗?答案是肯定的,你只需要掌握映射类型就可以了,它可以用来在原来对象类型的基础上为每个健添加一些新的属性或者去除某些属性从而派生出一个新的类型。我们先来看看上面定义的三个类型它们的转换关系是怎样的。
User类型到UserPartial类型的转换:
User类型到ReadonlyUser类型的转换:
那么如何用映射类型表达出这种转换关系呢?不着急,我们先来看看转换类型的语法:
在上面的表达式中P in K和JavaScript的for...in语句是类似的,它可以用来遍历K类型所有的键(key)以及这些键对应的值类型(T),T可以是TypeScript的任意类型。
你可以在这次遍历的过程中为K类型的键添加或者移除一些额外的像readonly和问号(?)这样的修饰词(modifier)。使用加号(+)可以为属性添加修饰词,相反使用减号(-)可以移除修饰词,如果你没有指定的话,TypeScript默认行为是为键添加某个修饰词。
综上所述,我们可以总结出常用的映射类型的语法下面这些:
{ [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 }
介绍完映射类型的语法后,我们再来看一些具体的例子加深一下理解:
好了看完了映射类型的基本介绍, 我们再来看一下如何使用映射类型来优化一下上面提到的UserPartial类型的定义:
type MyPartial<T> = {
[P in keyof T]?: T[P];
};
type UserPartial = MyPartial<User>;
在上面的代码中,我们定义了一个叫做MyPartial的映射类型,这个映射类型可以将User类型映射为UserPartial类型。keyof操作符的作用是获取某个对象类型的所有keys,它是一个联合类型。在in操作符每次遍历的过程中,P会依次变成User的每个键(key),然后 T[P] 代表的就是当前键对应的值的类型。
接着我们使用动画的形式来演示一下MyPartial映射类型的执行过程,如果你还不是很明白的话,可以多看几次直到彻底理解映射类型的概念。
TypeScript 4.1还允许我们使用as子句将映射类型的keys进行重新映射。它的语法是这样的:
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类型的子类型,因为我们必须使用 & 操作符来对类型进行过滤。
另外,在对keys进行重映射的过程中,我们可以通过返回never类型来过滤掉某些keys。
// 删除'kind'属性
type RemoveKindField<T> = {
[K in keyof T as Exclude<K, "kind">]: T[K]
};
interface Circle {
kind: "circle";
radius: number;
}
type KindlessCicle = RemoveKindField<Cicle>;
// type KindlessCircle = {
// radius: number;
// };
好了映射类型就介绍到这里了,看完这篇文章,我相信你肯定已经理解映射类型是拿来做什么的了,并且也知道TypeScript的某些工具类型是如何实现的了。
转载自:https://juejin.cn/post/7220061751400284215