TypeScript 工具类型实战:Pick、Omit、Partial、Record、Exclude
在大型 TypeScript 项目中,我们经常会遇到这样的需求:从一个已有的类型里,挑出部分字段、剔除部分字段、或者把字段都变成可选。如果每次都手写一个新 interface,不仅重复劳动,而且一旦源类型改了,派生类型也要跟着改,维护成本很高。
TypeScript 内置了一套「工具类型」(Utility Types),专门用来做这类类型的变形和裁剪。本文以一段实际代码为线索,把这些工具类型的用法和它们之间的关系讲清楚。
0. 一个统一的源类型
下面所有例子都围绕同一个 User 接口展开:
interface User {
id: number;
name: string;
age: number;
email: string;
}
1. Pick<T, K>:挑选字段,形成新类型
Pick 从类型 T 中挑选你需要的字段,组成一个新的类型。
有什么特性?从一个类型挑选一些你需要的字段,形成新的类型。大型项目里类型消费较多,这种场景很常见。
type UserPreview = Pick<User, 'id' | 'name'>
const u: UserPreview = {
id: 1,
name: 'c三',
}
这里 UserPreview 只保留了 id 和 name,age、email 都不允许出现。适合「列表项只需要展示部分字段」的场景,比如卡片预览、下拉选项等。
2. Omit<T, K>:排除字段,形成新类型
Omit 和 Pick 正好相反,它是从类型 T 中排除指定的字段,保留剩下的。
type UserSafe = Omit<User, 'email' | 'age'>
const safe: UserSafe = {
id: 2,
name: 'a',
}
UserSafe 去掉了 email 和 age,只剩下 id 和 name。典型场景是「脱敏」:把一个内部类型去掉敏感字段(比如 password、email),返回给前端。
3. Omit 与 Pick 的等价关系
readme 里点出了最关键的一层关系:
Omit<T, K>等价于Pick<T, Exclude<keyof T, K>>
拆开理解:
keyof T:拿到T所有键的联合类型;Exclude<keyof T, K>:从所有键里排除掉K中的键,保留剩下的;Pick<T, ...>:再把这些剩下的键挑出来。
用代码验证一下(2.ts 就是这个思路的手工展开版):
type UserKeys = keyof User; // 'id' | 'name' | 'age' | 'email'
type KeepKeys = Exclude<UserKeys, 'email'>; // 'id' | 'name' | 'age'
type MyOmitUser = Pick<User, KeepKeys>; // 等价于 Omit<User, 'email'>
所以 Omit 本质上是 Pick + Exclude 的组合,理解了这一点,这几个工具类型就能串起来了。
4. Exclude<T, U>:从联合类型里剔除
Exclude 处理的是联合类型(union),而不是对象接口。注意和 Omit 的分工:
Omit处理对象接口,删除的是接口里的字段;Exclude处理联合类型,剔除的是联合类型里的某个成员。
type All = 'id' | 'name' | 'age' | 'email';
type AfterExclude = Exclude<All, 'email'>; // 'id' | 'name' | 'age'
5. Partial<T>:把所有属性变成可选
Partial 把类型 T 的所有属性都设置为可选。
type PartialUser = Partial<User>
// patch 修改 —— 只传需要改的字段即可
const pa: PartialUser = {
id: 3,
name: 'b',
}
// 甚至可以传空对象
const emptyObj: PartialUser = {}
Partial 最典型的场景就是「局部更新」(patch / PATCH 接口):更新用户时只传要修改的字段,其它字段不用带上。因为所有属性都可选,传空对象也是合法的。
6. Record<K, V>:键值对类型
Record 用来描述一个「键类型 → 值类型」的映射,语法是 Record<键类型, 值类型>。
type Dict = Record<string, number>;
const obj: Dict = { a: 1, b: 2 };
更实用的例子是错误码映射表:
type ErrorMsgMap = Record<number, string>;
const errorMessage: ErrorMsgMap = {
400: '请求参数错误',
401: '未登录,请重新登录',
403: '没有权限访问',
404: '资源未找到',
500: '服务器错误', // 500 状态码 http status code
}
function getErrorMsg(code: number): string {
return errorMessage[code] ?? '未知错误'
}
这里 code 是 HTTP 状态码(数字),值是对应的错误文案(字符串),Record<number, string> 完美表达了这种「字典」结构。
7. ReturnType<T>:推导函数返回值类型
ReturnType 用来拿到某个函数的返回值类型,不需要手动声明:
function fn() { return { x: 1, y: 2 } };
type fnReturn = ReturnType<typeof fn>; // { x: number, y: number }
注意要配合 typeof fn 使用:ReturnType 接受的是函数类型,typeof fn 才拿到 fn 的类型。
小结:一张关系图
| 工具类型 | 作用 | 处理对象 |
|---|---|---|
Pick<T, K> | 挑出指定字段 | 对象接口 |
Omit<T, K> | 排除指定字段 | 对象接口 |
Partial<T> | 所有属性变可选 | 对象接口 |
Exclude<T, U> | 剔除联合类型成员 | 联合类型 |
Record<K, V> | 定义键值映射 | 字典结构 |
ReturnType<T> | 推导函数返回值 | 函数类型 |
keyof T | 取所有键的联合类型 | 基础操作 |
核心记法:Omit<T, K> = Pick<T, Exclude<keyof T, K>>。把这一个等价关系吃透,Pick、Omit、Exclude、keyof 之间的关系就都通了。
使用心法:优先用工具类型去「派生」新类型,而不是重复手写接口。这样源类型一改,所有派生类型自动跟着变,既省事又不容易漏。