摘要
从Pick到Record,详解TS五大工具类型------Pick挑选字段、Omit排除字段、Partial全部可选、Record字典类型、ReturnType提取返回值类型,并拆解Exclude+keyof实现Omit的内部原理。
为什么需要工具类型?
你接手一个5年前的老项目,User 接口定义了20个字段。列表页只展示 id 和 name,详情页要显示除了 email 之外的所有信息,编辑表单需要全部字段但必须全部可选。
你的选择是:
- A. 手写3个重复的 interface,改一个字段要同步改3处
- B. 直接复用
User接口,然后在代码里用as any强行绕过类型检查 - C. 用 TypeScript 工具类型,一行代码搞定
正确答案是 C。
TypeScript 内置的工具类型(Utility Types)就是为这类场景设计的------基于已有类型,通过类型运算产生新类型,避免重复定义和类型污染。
typescript
typescript
interface User {
id: number;
name: string;
age: number;
email: string;
}
以下所有示例都基于这个 User 接口。
Pick:从接口中挑选字段
Pick<T, K> 从类型 T 中挑选出 K 指定的字段,形成一个新的类型:
typescript
ini
type UserPreview = Pick<User, 'id' | 'name'>;
const u: UserPreview = {
id: 1,
name: '张三',
};
Pick<User, 'id' | 'name'> 从 User 中只取 id 和 name 两个字段,返回的新类型只包含这两个字段。'id' | 'name' 是联合类型字符串,表示"id 或 name"。
第二个参数必须是 T 中存在的字段名,否则 TypeScript 会报错。这是类型安全的保障------不能挑选接口中不存在的字段。
典型场景:列表页只需要展示用户的 id 和 name,不需要 age 和 email,用 Pick 精准控制返回给前端的数据结构。
Omit:从接口中排除字段
Omit<T, K> 从类型 T 中排除 K 指定的字段,保留其余所有字段:
typescript
ini
type UserSafe = Omit<User, 'email'>;
const safeUser: UserSafe = {
id: 1,
name: '张三',
age: 11,
};
Omit<User, 'email'> 从 User 中去掉 email 字段,保留 id、name、age。
典型场景:API 返回用户信息时需要脱敏,去掉 email、password 等敏感字段,用 Omit 一次完成排除。
Omit 的内部实现:Exclude + keyof + Pick
Omit 的内部实现等价于 Pick<T, Exclude<keyof T, K>>。拆解这个表达式就能理解 Omit 的工作原理:
typescript
bash
type UserKeys = keyof User; // "id" | "name" | "age" | "email"
type KeepKeys = Exclude<UserKeys, "email">; // "id" | "name" | "age"
type MyOmitUser = Pick<User, KeepKeys>; // 只包含 id、name、age
三步走拆解:
keyof User拿到 User 所有键的联合类型"id" | "name" | "age" | "email"Exclude<联合类型, "email">从联合类型中删除 "email",剩下"id" | "name" | "age"Pick<User, 保留的键>从 User 中挑选出这些键,得到排除了 email 的新类型
Exclude<联合类型, 要删除的成员> 处理的是联合类型 (union),Omit<T, K> 处理的是对象接口 。Omit 内部先用 keyof 把对象键转成联合类型,再用 Exclude 删除指定键,最后用 Pick 重建对象类型。
Partial:所有字段变为可选
Partial<T> 把类型 T 的所有字段变成可选:
typescript
ini
type PartialUser = Partial<User>;
const patchUser: PartialUser = {
name: '李四',
age: 12,
};
const emptyObj: PartialUser = {};
Partial<User> 相当于把 User 的每个字段都加上了 ?,变成 { id?: number; name?: string; age?: number; email?: string }。
典型场景 :PATCH 请求------前端只提交修改的字段,不必传完整的 User 对象。PartialUser 类型的对象可以只传 name 和 age,也可以传空对象,TypeScript 不会报错。
与 Partial 对称的是 Required<T>,它将所有可选字段变为必选,适合处理配置对象默认值的场景。
Record:字典类型
Record<K, V> 创建一个键类型为 K、值类型为 V 的对象类型:
typescript
ini
type Dict = Record<string, number>;
const obj: Dict = { a: 1, b: 2 };
Record<string, number> 表示键是字符串、值是数字的字典结构。obj 可以添加任意字符串键,值必须是数字。
典型场景:映射表,比如 HTTP 状态码和错误消息的对应关系:
typescript
typescript
type ErrorMsgMap = Record<number, string>;
const errorMsgMap: ErrorMsgMap = {
400: '请求参数错误',
401: '未登录,请重新登录',
403: '权限不足,禁止访问',
404: '资源找不到',
500: '服务器内部错误',
};
function getErrorMsg(code: number): string {
return errorMsgMap[code] ?? '未知错误';
}
Record<number, string> 约束了键必须是数字、值必须是字符串。errorMsgMap[code] ?? '未知错误' 中的 ?? 是空值合并运算符,当 errorMsgMap[code] 为 undefined 时返回默认值。
ReturnType:获取函数返回值类型
ReturnType<T> 提取函数类型的返回值类型:
typescript
csharp
function fn() { return {x: 1, y: 2}; }
type FnReturn = ReturnType<typeof fn>;
// FnReturn 的类型是 { x: number; y: number }
typeof fn 获取函数的类型,ReturnType 提取该函数的返回值类型。
典型场景 :封装第三方库时,不需要手动声明返回值接口,直接从函数推导。比如封装一个 API 请求函数,用 ReturnType 推导返回值类型,保持类型与实现同步。
工具类型对比总结
| 工具类型 | 作用 | 典型场景 | 示例 |
|-----------------|-----------|------------------|--------------------------|------------|
| Pick<T, K> | 挑选指定字段 | 列表页只展示部分字段 | `Pick<User, 'id' | 'name'>` |
| Omit<T, K> | 排除指定字段 | 返回数据脱敏(去掉 email) | Omit<User, 'email'> |
| Partial<T> | 全部字段变为可选 | PATCH 请求体 | Partial<User> |
| Record<K, V> | 创建字典类型 | 状态码映射、枚举映射 | Record<number, string> |
| ReturnType<T> | 提取函数返回值类型 | API 响应类型推导 | ReturnType<typeof fn> |
总结
TypeScript 工具类型通过类型运算从已有类型派生新类型,避免了重复定义接口。
Pick 和 Omit 是"选"与"删"的对称操作------Pick 指定要保留的字段,Omit 指定要排除的字段。Omit 内部等价于 Pick<T, Exclude<keyof T, K>>,体现了 Exclude 处理联合类型、Omit 处理对象接口的分工。
Partial 让所有字段可选,适配 PATCH 场景;Required 与其对称,让所有字段必选。Record 提供字典类型约束;ReturnType 从函数推导返回值类型,保持类型与实现同步。
掌握这些工具类型,能减少大量重复的类型定义代码,让 TypeScript 项目更优雅、更安全。