探秘Typescript·常用的工具类型及其底层原理

背景

探秘Typescript·常用的工具类型及其底层原理

我们已经了解了Typescript当中的一些常见的日常类型了,也知道了在Typescript当中,有着极为强大的类型计算体系。使用这些日常类型,我们基本可以完成日常开发工作的80%左右的问题,而剩下的一些问题,都可以通过Typescript当中的强大的类型计算体系来解决。但大多数是情况下,很多的场景,Typescript已经帮我们考虑到了一些复杂场景,为我们预置了大量的工具类型,用以辅助我们完成更为复杂的类型计算。今天,我们就来详细得聊一聊这些预置的工具类型及其底层实现的原理。

Partial – 所有属性变可选

Partial工具类型,可以将原本类型中的所有属性变成可选属性,方便我们更好的利用现有的类型演变出新的类型,例如:

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

let user: User = {
  name: "kiner",
  age: 20
};

function updateInfo(newUser: Partial<User>): void {
  user = {
    ...user,
    ...newUser
  };
}

// 以下的表述方式是等效的
// Partial<User> = {name?: string; age?: number};

updateInfo({
  name: "tang"
});
console.log(user);// {name: "tang", age: 20}
updateInfo({
  age: 18
});
console.log(user);// {name: "tang", age: 18}

从上面的示例中我们可以看到,我们有一个基础的用户类型User,我们期望在定义updateInfo方法时,接受的参数,只要包含User中的任意属性即可,无需传入所有属性。如果不使用Partial工具,那么,我们可能还需要额外定一个新的类型去维护newUser参数的类型,但使用Partial后,我们就可以直接用User计算出newUser的类型,非常方便快捷,还可以有效利用已有类型进行计算,一旦User对象的类型结构发生了改变,如增加了一个属性sex,此时我们的newUser的类型完全不用手动更改,自动就会支持新属性。

我们说过,Typescript的类型都是可计算的,Partial工具本质上可以看做是一个语法糖,封装了一些便捷的类型计算逻辑,那么,它底层是如何实现的呢?

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

我们可以看到,本质上,Partial工具就是将传入的泛型变量T当中的每一个属性都多加一个可选标记?而已。就这么简单

Required – 所有属性变必选

Partial相反的,如果我们想让一个类型的所有属性都变味必选,就可以使用Required,如:

type User = {
  name?: string;
  age?: number;
  sex: string;
}
type AddUserParams = Required<User>;
// {name: string; age: number; sex: string;}

我们再来看看,Required的底层实现逻辑:

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

我们可以看到,该方法其实就是将传入的泛型参数当中的所有属性都进行了-?操作,在Typescript当中,-?操作代表去除当前属性的可选标记。

Readonly – 所有属性标记为只读

在实际开发过程中,有一些数据我们不想被修改,就会将这些数据标记为readonly,如果在一个类型所有属性都要标记为只读,那我们可以这样:

type User = {
  name: string;
  age: number;
}
type ReadonlyUser = Readonly<User>;
// {readonly name: string; readonly age: number}

其实底层原理跟上面的工具也都是很类似的:

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

Pick – 从一个类型中挑选目标属性

如果我们想要从一个类型当中挑选出若干个目标属性出来,我们可以使用此工具,如:

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

type NewUser = Pick<User, "name" | "age">;
// {name: string, age: number}
const newUser: NewUser = {
  name: "kiner",
  age: 20
}

从上述示例中,我们可以看到,我们从User当中挑选了nameage属性出来形成了新的类型NewUser,此时,NewUser当中就只有这两个属性,没有sex属性了。

那么,我们再来看一下这个工具的原理吧:

/**
 * 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];
};

我们可以看到,该工具从传入的泛型参数T当中,挑选T的键值子集范围的属性形成新的类型。这么说或许不太好理解,我们还是拿上面的示例来说。类型User键值的全集是name | age | sex,而我们传入的name | age则是全集的一部分,也就是其子集,因此,上述的表达式的意思就是:User当中挑选出nameage属性出来形成一个新的类型

Record – 记录(json)类型

如果我们想要在Typescript当中描述一个键值都不确定的对象时,可以使用这个工具进行描述,它允许我们规定对象类型允许的键的类型以及所对应值的类型,如:

type Column = Record<string, string>;
const column: Column = {
  name: "kiner",
  age: 20,// 报错,因为我们规定了`Column`类型的值只能是`string`类型,如果要不报错,可以将类型定义为:Record<string, string | number>
  20: "tang"// 报错,因为我们规定了`Column`类型的键名只能是`string`类型
};

这样,我们就能够定义一个起初不确定键名的对象了。那么,我们还是来看一下,这个工具,底层是如何实现的:

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

我们可以看到,只要我们传入的对象的键的类型满足继承于keyof anystring | number | symbol,并且值的类型是T即可。

Exclude – 将子集从全集当中排除

获取我们有些开发场景需要实现将全集中的某些元素排除在外,仅保留其余元素的情况,此时我们便可以使用这个工具,如:

type All = "name" | "age" | "sex";
type OnlySex = Exclude<All, "name" | "age">;// "sex";

我们可以看到上述的例子当中,将nameage从全集当中排除,因此,新类型OnlySex当中就只有sex属性了。

我们来看看这个工具是如何实现的:

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

这边只需要判断T是否是继承与U的,如果不是继承于U的,那么我们就保留这个属性,否则设置为永远不可达的类型never.即:该工具类型能够从类型T中剔除所有可以赋值给类型U的类型

Extract – 获取两个集合的交集

此工具实际跟Exclude是互补的,它能够从类型T中获取所有可以赋值给类型U的类型。如:

type All1 = "name" | "age" | "sex";
type All2 = "name" | "sex" | "weight";
type All3 = Extract<All1, All2>;// All1和All2 相交的部分是"name"和"sex",因此,All3的类型应该是"name" | "sex"

再来看看这个工具的底层原理吧:

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

从实现上看,与Exclude相比,仅仅只是将结果交换了而已。

Omit – 从类型中剔除部分属性

我们上面说的Exclude类型,通常使用与联合类型排除相交部分的场景,但如果想要将一个对象类型的某些属性剔除,应该使用Omit,如:

type User = {
  id: number;
  name: string;
  age: number;
  sex: string;
};

type AddUser = Omit<User, "id">;

例如我们定义了一个用户对象,用户本身是存在id属性的,但是在新增用户的场景时,id还没有生成,因此,在新增用户的时候,需要传除了id之外的其他参数,那么,我们就可以用这个工具将id属性从User当中剔除掉。

那么,我们再来看看这个工具又是如何实现的呢?

/**
 * 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>>;

我们可以看到,这个工具的实现相交于上述的一些工具相对复杂一些,同时还是用到了上面讲到的Pick工具和Exclude工具。其思路大体是这样的:从T当中挑选出除了给定的属性之外的其他属性出来,而指定需要剔除的属性也许要满足是键值的合法类型,即keyof any,也就是string | number | symbol

Parameters – 获取函数的参数类型

有些时候,我们需要根据已经定义的函数获取他的参数类型,此时我们可以使用这个工具。如:

function showInfo(info: {name: string, age: number, sex?: string}) : void {
  console.log(info);
}

type ShowInfoParams = Parameters<typeof showInfo>;
// [info: {
//     name: string;
//     age: number;
//     sex?: string | undefined;
// }]

如上述示例,我们很轻易的就获取到了showInfo方法的传入参数了。

我们来看看这个工具底层是如何实现的吧:

/**
 * 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;

此处使用到了之前讲过的Typescript当中的高级计算技巧infer对类型进行推导,直接推导出目标函数的参数类型,我们可以看到,上面使用了...运算符收集了所有参数的信息,这也是为什么我们上面推导出来的函数参数是一个数组的原因。

ReturnType – 获取函数的返回值

既然能够推导函数的传入参数,有进必有出,自然也可以推导出函数的返回值类型了。我们想要推导函数的返回值只需要使用ReturnType工具即可,如:

function showInfo(info: {name: string, age: number, sex?: string}) : string {
  console.log(info);
  return info.name;
}

type ShowInfoReturnType = ReturnType<typeof showInfo>;// string

如上面的示例,我们很轻易的就推导出了showInfo函数的返回值类型是string。实际上,其底层原理与Parameters差不多:

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

结语

今天我们暂且介绍这么多常用的工具类,实际上,在Typescript当中,提供的工具类远不止于这些,大家有兴趣可以自行查阅相关资料学习了解。个人的想法是,我们只需掌握上述常用的工具类即可,其他更多的工具,我们只需要有一个印象,等实际使用时再查阅相关文档即可,无需死记硬背。希望这些常用工具能让你再编写自己的类型系统时如虎添翼,更加快捷高效的定义类型,为项目的高质量稳定迭代保驾护航。

原文链接:https://juejin.cn/post/7327570230774857762 作者:kinertang

(0)
上一篇 2024年1月25日 上午10:28
下一篇 2024年1月25日 上午10:38

相关推荐

发表回复

登录后才能评论