22

作者: Marius Schulz

原文链接: https://mariusschulz.com/blog/the-omit-helper-type-in-typescript

3.5 版本之后,TypeScript 在 lib.es5.d.ts 里添加了一个 ​Omit<T, K>​ 帮助类型。​Omit<T, K>​ 类型让我们可以从另一个对象类型中剔除某些属性,并创建一个新的对象类型:

type User = {

id: string;

name: string;

email: string;

};

type UserWithoutEmail = Omit<User, "email">;

// 等价于:

type UserWithoutEmail = {

id: string;

name: string;

};

而在 lib.es5.d.ts 里面 ​Omit<T, K>​ 帮助类型长这样:

_/**_

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

把这个类型定义解释清楚并且明白其中的原理,我们可以试着实现一个自己的版本,来还原它的功能。

定义 Omit<T, K> 帮助类型

我们先从上面的那个 ​User​ 类型开始:

type User = {

id: string;

name: string;

email: string;

};

首先,我们需要找到所有 ​User​ 类型中的属性名。我们可以用 ​keyof操作符来获取一个包含所有属性名字字符串的联合属性:

type UserKeys = keyof User;

_// 等价于:_

type UserKeys = "id" | "name" | "email";

然后我们需要能够把联合属性中的某个字符串字面量剔除出去的能力。那我们的 ​User​ 类型举例的话,我们想要从 ​"id" | "name" | "email"​ 中去掉 ​"email"​ 。我们可以用 ​Exclude<T, U>​ 帮助类型来做这件事:

type UserKeysWithoutEmail = Exclude<UserKeys, "email">;

_// 等价于:_

type UserKeysWithoutEmail = Exclude<

"id" | "name" | "email",

"email"

>;

_// 等价于:_

type UserKeysWithoutEmail = "id" | "name";

​Exclude<T, U>​lib.es5.d.ts 里面是这样定义的:

/**

 * Exclude from T those types that are assignable to U

 */

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

它用了一个条件类型和 ​never类型。用 ​Exclude<T, U>​ 实际上我们在从联合类型​"id" | "name" | "email"​ 中去掉那些匹配 ​"email"​ 类型的类型。而匹配 ​"email"​ 类型的只有 ​"email"​ ,所以就剩下了· ​"id" | "name"​ 。

最后,我们需要创意一个对象类型,包含 ​User​ 类型属性子集的对象类型。其实更具体的说,就是要创建一个对象类型,它的属性都是在联合类型 ​UserKeysWithoutEmail​ 中的。我们可以用 ​Pick<T, K>​ 帮助类型来挑出来所有对应的属性名:

type UserWithoutEmail = Pick<User, UserKeysWithoutEmail>;

// 等价于:

type UserWithoutEmail = Pick<User, "id" | "name">;

// 等价于:

type UserWithoutEmail = {

id: string;

name: string;

};

而 ​Pick<T, K>​ 帮助类型是这样定义的:

/**

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

};

​Pick<T, K>​ 帮助类型是一个映射类型,它用了 ​keyof​ 操作符和一个索引类型T[P]​ 来获取类型对象类型 ​T​ 中的属性 ​P​ 。

现在,我们来把上面提到的 ​keyof​ ,​Exclude<T, U>​ 和 ​Pick<T, K>​ 整合成一个类型:

type UserWithoutEmail = Pick<User, Exclude<keyof User, "email">>;

值得注意的是这样的写法只能应用到我们定义的 ​User​ 类型中。我们加入一个范型,就能让它用在其他地方了:

type Omit<T, K> = Pick<T, Exclude<keyof T, K>>;

现在,我们可以计算出我们的 ​UserWithoutEmail​ 类型了:

type UserWithoutEmail = Omit<User, "email">;

因为对象的键只能是字符串、数字或 Symbol,那么我们可以给 ​K​ 加个约束条件:

type Omit<T, K extends string | number | symbol> = Pick<T, Exclude<keyof T, K>>;

这样直接约束 ​extends string | number | symbol​ 看上去有点啰嗦了。我们可以用 ​keyof any​ 来实现,因为它们是等价的:

type Omit<T, K extends keyof any> = Pick<T, Exclude<keyof T, K>>;

于是我们现在完成了!我们已经实现了 lib.es5.d.ts 中定义的 ​Omit<T, K>​ 类型了:

_/**_

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

拆解 Omit<User, "email">

下面这段代码就是逐步拆解的 ​Omit<User, "email">​ 类型。试着跟随每个步骤并理解 TypeScript 是如何计算出最终的类型的:

type User = {

id: string;

name: string;

email: string;

};

type UserWithoutEmail = Omit<User, "email">;

// 等价于:

type UserWithoutEmail = Pick<

  User,

Exclude<keyof User, "email">

>;

// 等价于:

type UserWithoutEmail = Pick<

  User,

  Exclude<"id" | "name" | "email", "email">

>;

// 等价于:

type UserWithoutEmail = Pick<

  User,

  | ("id" extends "email" ? never : "id")

  | ("name" extends "email" ? never : "name")

  | ("email" extends "email" ? never : "email")

>;

// 等价于:

type UserWithoutEmail = Pick<User, "id" | "name" | never>;

// 等价于:

type UserWithoutEmail = Pick<User, "id" | "name">;

// 等价于:

type UserWithoutEmail = {

[P in "id" | "name"]: User[P];

};

// 等价于:

type UserWithoutEmail = {

  id: User["id"];

  name: User["name"];

};

// 等价于:

type UserWithoutEmail = {

id: string;

name: string;

};

齐活,我们的 ​UserWithoutEmail​ 类型。


前端魔法师
82 声望7 粉丝