TypeScript 工具类型实战:从 Pick、Omit 到 Partial、Record 与 ReturnType

1 阅读9分钟

TypeScript 工具类型实战:从 Pick、Omit 到 Partial、Record 与 ReturnType

在 TypeScript 项目中,同一份数据经常会以不同的形态出现。

例如,一个完整的用户对象可能包含 id、姓名、年龄和邮箱;展示用户摘要时只需要其中两个字段;更新用户资料时又只会提交发生变化的字段。如果为每一种场景都重新声明一套接口,不仅重复,而且容易在原始类型变化后漏掉同步修改。

TypeScript 提供的工具类型,正是用来根据已有类型推导新类型的。本文围绕下面这个用户接口,依次讲清楚 Pick、Omit、Partial、Record、ReturnType 和 Exclude,最后再把 Omit 的工作过程完整拆开。

interface User {
  id: number;
  name: string;
  age: number;
  email: string;
}

一、Pick:从对象类型中挑选字段

假设某个位置只展示用户的编号和姓名,并不需要完整的 User。这时可以使用 Pick:

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

const u: UserPreview = {
  id: 1,
  name: "luoyue",
};

Pick 可以理解为“挑选”,它接收两个类型参数:

Pick<原始对象类型, 需要保留的键>

第二个参数 "id" | "name" 是一个联合类型,表示需要从 User 中保留 id 和 name。因此,UserPreview 最终可以理解为:

type UserPreview = {
  id: number;
  name: string;
};

这里不是重新编写了一份互不相关的类型,而是以 User 为基础选出了需要的字段。字段名称、字段类型都来自原始类型。

当原始对象字段很多,而当前场景只消费其中少数字段时,Pick 的表达最直接:明确写出“我要哪些字段”。

二、Omit:从对象类型中排除字段

有时需要保留大部分字段,只去掉少量内容。例如,不希望在某个对象类型中出现邮箱,可以使用 Omit:

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

const safeUser: UserSafe = {
  id: 2,
  name: "luoyue",
  age: 21,
};

Omit 可以理解为“省略”或“排除”,写法是:

Omit<原始对象类型, 需要排除的键>

从 User 中去掉 email 后,UserSafe 相当于:

type UserSafe = {
  id: number;
  name: string;
  age: number;
};

Pick 和 Omit 都是在已有对象类型的字段上做选择,区别只是思考方向不同:

工具类型写出的字段更适合的情况
Pick要保留的字段只需要原类型中的少数字段
Omit要排除的字段只需要从原类型中去掉少数字段

对于同一个 User,如果只要 id 和 name,用 Pick 更清楚;如果只想去掉 email,用 Omit 更简洁。

三、Partial:让所有字段变成可选字段

Pick 和 Omit 改变的是“保留哪些字段”,Partial 改变的则是“字段是否必须提供”。

type PartialUser = Partial<User>;

Partial<User> 会把 User 的全部字段变成可选字段,因此 PartialUser 可以理解为:

type PartialUser = {
  id?: number;
  name?: string;
  age?: number;
  email?: string;
};

这非常适合表示“只修改部分属性”的数据。一个用户对象可能有很多属性,但一次修改通常只涉及其中几个:

const patchUser: PartialUser = {
  name: "luoyue",
  age: 21,
};

这里没有传入 id 和 email,因为四个字段都已经变成可选字段。甚至空对象也符合 PartialUser:

const emptyObj: PartialUser = {};

需要注意的是,Partial 只让字段变成可选,并没有改变字段本身的值类型。age 如果出现,仍然应该是 number;name 如果出现,仍然应该是 string。

因此可以把 Partial 的作用概括为:对象可以只提供一部分字段,但提供出来的字段仍要遵守原类型的约束。

四、Record:描述键值映射关系

JavaScript 中经常会使用对象保存“键—值”映射。TypeScript 可以用 Record 描述这类结构:

type Dict = Record<string, number>;

const obj: Dict = {
  a: 1,
  b: 2,
};

Record 的两个类型参数分别表示键类型和值类型:

Record<键类型, 值类型>

在 Record<string, number> 中,键是字符串,值是数字,所以 a: 1、b: 2 都满足约束。

另一个典型场景是根据 HTTP 状态码查找错误提示:

type ErrorMsgMap = Record<number, string>;

const errorMessage: ErrorMsgMap = {
  400: "请求参数错误",
  401: "未登录,请重新登录",
  403: "权限不足,禁止访问",
  404: "资源找不到",
  500: "服务器内部错误",
};

这个类型表示:使用数字作为键,使用字符串作为对应的提示信息。上面的状态码中,400、401、403、404 属于 4xx 范围,表示客户端请求出现问题;500 属于 5xx 范围,表示服务器内部错误。

HTTP 状态码按范围可以作如下区分:

范围含义
1xx请求正在处理
2xx请求成功
3xx需要重定向
4xx客户端请求错误
5xx服务器一侧出现错误

有了这份映射,就可以通过状态码获取提示:

function getErrorMsg(code: number): string {
  return errorMessage[code] ?? "未知错误";
}

errorMessage[code] 根据数字状态码查找字符串,?? "未知错误" 则为没有配置提示的状态码准备一个默认结果。这样无论是否找到对应内容,函数最终都会返回字符串。

Record 的重点不在于某一个具体属性,而在于统一规定一组键和值之间的类型关系。

五、ReturnType:提取函数的返回值类型

除了从对象类型中选择字段,TypeScript 还可以从函数中提取类型。先定义一个函数:

function fn() {
  return { x: 1, y: 2 };
}

TypeScript 会根据 return 后面的对象推断这个函数的返回值形状。要把它单独提取为一个类型,可以使用 ReturnType:

type fnReturn = ReturnType<typeof fn>;

这段代码可以分成两步理解:

  1. typeof fn 获取函数 fn 的类型。
  2. ReturnType<...> 从这个函数类型中提取返回值类型。

所以,fnReturn 最终可以理解为:

type fnReturn = {
  x: number;
  y: number;
};

这里不能只写 ReturnType<fn>,因为 fn 是一个函数值,而 ReturnType 接收的是类型。typeof fn 完成了从函数值到函数类型的转换。

这种写法让返回值类型直接来自函数本身,不需要再手动写一份相同的对象类型。

六、Exclude:从联合类型中排除成员

Exclude 也表示“排除”,但它处理的是联合类型,而不是对象字段。

先定义一个由多个字符串字面量组成的联合类型:

type All = "id" | "name" | "age" | "email";

如果要从中排除 "email",可以这样写:

type AfterExclude = Exclude<All, "email">;

得到的结果是:

type AfterExclude = "id" | "name" | "age";

因此,Exclude 的使用方式可以概括为:

Exclude<原联合类型, 需要排除的联合成员>

它和 Omit 看起来很像,但两者处理的目标不同:

工具类型处理对象作用
Exclude联合类型从联合类型中去掉成员
Omit对象类型从对象类型中去掉属性

例如,All 本身就是一组联合成员,所以可以直接交给 Exclude;User 是对象类型,需要使用 Omit 去掉它的属性。

七、拆解 Omit:keyof、Exclude 与 Pick 如何配合

Omit<User, "email"> 的效果,可以通过 keyof、Exclude 和 Pick 一步一步得到。这也是理解三个工具之间关系的关键。

第一步:用 keyof 取得所有键

type UserKeys = keyof User;

keyof User 会取得 User 的所有属性名,形成一个联合类型:

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

此时处理目标已经从“一个对象类型”变成了“一个由属性名组成的联合类型”。

第二步:用 Exclude 排除不需要的键

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

Exclude 从全部属性名中去掉 "email",于是 KeepKeys 的结果是:

type KeepKeys = "id" | "name" | "age";

第三步:用 Pick 取回保留的字段

现在已经知道需要保留哪些键,再将它们交给 Pick:

type MyOmitUser = Pick<User, KeepKeys>;

最终得到的 MyOmitUser 就是:

type MyOmitUser = {
  id: number;
  name: string;
  age: number;
};

这与 Omit<User, "email"> 得到的结果一致。整个过程可以压缩成一条清晰的推导链:

User
  ↓ keyof:取得全部属性名
"id" | "name" | "age" | "email"
  ↓ Exclude:排除 "email"
"id" | "name" | "age"
  ↓ Pick:按保留的键选择属性
{ id: number; name: string; age: number }

因此,可以从思路上把 Omit<T, K> 理解成:

Pick<T, Exclude<keyof T, K>>

也就是:先获取对象的全部键,再排除不需要的键,最后从原对象中挑出剩余字段。

八、如何选择这些工具类型

这几个工具类型的名字不同,实际上分别回答了几种很具体的问题:

问题使用的工具类型示例结果
从对象中只保留哪些字段?Pick从 User 中保留 id、name
从对象中排除哪些字段?Omit从 User 中排除 email
如何让对象的全部字段都可选?Partial表示只修改部分用户属性
如何描述统一的键值映射?Record数字状态码映射字符串提示
如何取得函数返回值类型?ReturnType提取 { x: number; y: number }
如何从联合类型中排除成员?Exclude从属性名联合类型中排除 email

可以再用三个判断快速区分容易混淆的部分:

  • 面对对象类型,需要明确保留少数字段时,使用 Pick。
  • 面对对象类型,需要排除少数字段时,使用 Omit。
  • 面对联合类型,需要排除某些联合成员时,使用 Exclude。

总结

工具类型的价值,在对象字段越来越多、同一类型被多个位置消费时会更加明显。它们让新类型从已有类型中推导出来,减少重复声明,也把每个场景究竟需要什么表达得更清楚。

本文涉及的六个工具类型可以归纳为:

  • Pick:从对象类型中挑选字段。
  • Omit:从对象类型中排除字段。
  • Partial:把对象类型的全部字段变成可选字段。
  • Record:描述键类型和值类型之间的映射。
  • ReturnType:提取函数的返回值类型。
  • Exclude:从联合类型中排除成员。

其中最值得串起来理解的是:keyof 先把对象的键转换成联合类型,Exclude 再从联合类型中排除指定成员,Pick 最后按照剩余的键选择对象字段。这条链路一旦清楚,Omit 的作用就不再只是需要记忆的语法,而是一段可以逐步解释的类型推导过程。