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>;
这段代码可以分成两步理解:
typeof fn获取函数fn的类型。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 的作用就不再只是需要记忆的语法,而是一段可以逐步解释的类型推导过程。