第71课:内置工具类型(上)——Partial/Required/Readonly/Pick/Omit/Record
TypeScript 提供了二十余个内置工具类型(Utility Types),它们本质上是预先编写好的泛型类型,用于对现有类型进行变换——添加或移除修饰符、选取或排除属性、构造新的对象形状。理解这些工具类型的实现原理,不仅能让你更高效地使用它们,还能帮助你深入掌握映射类型、条件类型和 keyof 的配合运用。本节课将逐源码级拆解最常用的六个工具类型:Partial、Required、Readonly、Pick、Omit 和 Record,涵盖它们的定义、行为、使用场景和常见陷阱。
1. 属性修饰工具:Partial、Required、Readonly
这三个工具类型通过映射类型和修饰符来改变对象属性的可选性和可变性。它们的源码来自 TypeScript 的 lib.es5.d.ts。
1.1 Partial<T>:所有属性变为可选
Partial<T> 将类型 T 的所有属性变为可选。
源码级实现:
1 | type Partial<T> = { |
[P in keyof T]?: T[P] 遍历 T 的所有键,为每个属性添加 ? 修饰符,使其成为可选属性。属性值的类型保持不变(T[P])。
1 | interface User { |
典型场景:
- 表单部分更新:用户修改资料时,只提交被修改的字段。
- 配置合并:将用户配置与默认配置合并时,用户配置可以是
Partial<Config>。 - 对象构建器:逐步构造一个复杂对象,构造过程中允许属性缺失。
1 | function updateUser(id: number, changes: Partial<User>): void { |
1.2 Required<T>:所有属性变为必填
Required<T> 将类型 T 的所有属性变为必填(移除可选修饰符)。
源码级实现:
1 | type Required<T> = { |
-? 是映射类型中的修饰符移除语法:显式移除 ? 可选修饰符。+? 添加可选,-? 移除可选。
1 | interface Config { |
典型场景:
- 严格配置校验:在函数内部,将可选配置转为必填,确保所有字段都已填充默认值。
- 构造函数参数:某些构造阶段需要确保所有依赖已就绪。
1.3 Readonly<T>:所有属性变为只读
Readonly<T> 将类型 T 的所有属性变为只读(readonly)。
源码级实现:
1 | type Readonly<T> = { |
1 | interface Point { |
典型场景:
- 不可变数据:在函数式编程和 Redux 中,确保状态不可直接修改。
- 冻结对象:配合
Object.freeze()使用,类型层面也保证不可变。 - API 响应:从后端获取的数据通常不应被修改,用
Readonly<T>包装。
重要限制:Readonly<T> 是浅层的——只读仅作用于第一层属性。嵌套对象内部的属性仍可修改。
1 | interface User { |
如需深层只读,需要使用 DeepReadonly<T>(第 70 课已实现)或第三方库的类型。
2. 结构构造工具:Pick、Omit、Record
这三个工具类型用于构造新的对象形状——从已有类型中选取子集、排除子集,或从零开始构建键值对映射。
2.1 Pick<T, K>:选取属性子集
Pick<T, K> 从类型 T 中选取指定键 K 构成的类型。K 必须是 T 的键的联合子类型。
源码级实现:
1 | type Pick<T, K extends keyof T> = { |
K extends keyof T约束了K必须是T的属性键的子集。[P in K]: T[P]遍历K中的每个键,从T中获取其值类型。
1 | interface User { |
典型场景:
- API 响应裁剪:从完整的数据库模型中选取需要暴露给客户端的字段。
- 组件 Props 继承:从父组件的 Props 中选取子组件需要的部分。
- 权限控制:管理员视图拥有所有字段(
User),普通用户视图仅拥有部分字段(Pick<User, 'name' | 'email'>)。
2.2 Omit<T, K>:排除属性子集
Omit<T, K> 从类型 T 中排除指定键 K,返回剩余属性构成的类型。与 Pick 相反。
源码级实现:
1 | type Omit<T, K extends keyof any> = Pick<T, Exclude<keyof T, K>>; |
Omit 的实现结合了两个内置工具类型:
keyof T获取T的所有键。Exclude<keyof T, K>从所有键中排除K。Pick<T, ...>选取剩余的键。
K extends keyof any(而非 keyof T)的约束更宽松:允许排除可能不存在的键而不会报错(多余键会被忽略)。
1 | interface User { |
典型场景:
- 脱敏处理:从数据中排除密码、Token 等敏感字段。
- Props 转发:将父组件的 Props 排除掉子组件不需要的属性后转发。
- 表单差异:编辑表单缺少
id和createdAt等系统字段。
2.3 Record<K, V>:构造键值对对象
Record<K, V> 构造一个对象类型,其键的类型为 K,值的类型为 V。它是索引签名的泛型封装。
源码级实现:
1 | type Record<K extends keyof any, V> = { |
K extends keyof any约束K必须是合法的键类型(string | number | symbol)。- 映射类型
[P in K]: V遍历K的每个键,为每个键赋予相同的值类型V。
1 | // 字典:字符串键 → 字符串值 |
典型场景:
- 字典/映射表:键值对集合,如翻译词典、配置表。
- 枚举映射:将状态码或枚举值映射到对应的显示文本。
- 缓存存储:
Record<string, CacheEntry>。
2.4 Pick、Omit 与 Record 的对比
| 工具类型 | 输入 | 输出 | 逻辑 |
|---|---|---|---|
Pick<T, K> |
T(源类型)+ K(键) |
仅包含 K 中键的新对象类型 |
选取子集,从多到少 |
Omit<T, K> |
T(源类型)+ K(键) |
排除 K 中键的新对象类型 |
删除部分,从多到少 |
Record<K, V> |
K(键)+ V(值类型) |
全新的对象类型 | 从零构建,给定键和值 |
3. 综合实战:API 数据层类型设计
以下示例综合运用 Partial、Pick、Omit 和 Record,构建一个类型安全的 API 数据访问层。
1 | // 完整的数据库模型 |
设计要点:
Omit从完整模型中剥离系统字段(id、createdAt)得到CreateUserInput。Partial+Omit组合生成所有业务字段都可选的UpdateUserInput。Omit排除敏感字段password得到SafeUser(脱敏)。Record结合交叉类型构造嵌套的响应结构。
4. 最佳实践与常见陷阱
4.1 Partial 的误用:当你需要全部字段时
Partial<T> 让所有属性可选,但如果函数确实需要某些字段,应在类型层面明确声明,而不是让调用方猜测哪些字段是可选的。
4.2 Readonly 的浅层限制
记住 Readonly<T> 仅冻结第一层属性。深层不可变需要递归类型。
4.3 Record 的键兼容性
Record<string, V> 允许任意字符串键——包括那些不与具体属性对应的键。如果你需要对象具有特定的一组属性,使用联合类型作为 K 而非 string。
4.4 Omit 宽松的键约束
Omit<T, K> 中 K 的约束是 keyof any,而非 keyof T。这意味着你可以 Omit 一个不存在的属性而不报错(TypeScript 会忽略它)。这是一把双刃剑——提供了灵活性,但也可能掩盖拼写错误。如果需要严格约束,可在包装类型中显式检查。
课后练习
一、概念自测(选择题 / 填空题)
(单选)
Omit<T, K>的内部实现使用了哪两个工具类型的组合?
A.Partial<T> & Pick<T, K>
B.Pick<T, Exclude<keyof T, K>>
C.Required<T> | Record<K, any>
D.Readonly<Pick<T, K>>(单选)
Readonly<T>只读效果的“浅层”指的是什么?
A. 只能读取对象的原型属性。
B. 仅第一层属性变为只读,嵌套对象内部属性仍可修改。
C. 只能在函数内部使用。
D. 只读效果仅作用于数组元素。(填空) 要构造一个键为
'a' | 'b' | 'c',值类型均为boolean的对象类型,应使用______<'a' | 'b' | 'c', boolean>。(多选) 以下哪些工具类型使用了映射类型作为底层实现?
A.Partial<T>
B.Pick<T, K>
C.Record<K, V>
D.Exclude<T, U>
二、AI 编程任务:编写面向 AI 的提示词
场景:你需要为一个任务管理应用设计 TypeScript 类型层。要求如下:
- 定义完整的
Task接口,包含id(number)、title(string)、description(string)、status('todo' | 'in-progress' | 'done')、assignee(User接口)、tags(string 数组)、createdAt(Date)、updatedAt(Date)。 - 使用
Pick定义TaskPreview类型,仅包含id、title、status、assignee。 - 使用
Omit定义CreateTaskInput类型,排除id、createdAt、updatedAt。 - 使用
Partial结合Omit定义UpdateTaskInput类型,所有业务字段可选,且排除id、createdAt。 - 使用
Record定义TasksByStatus类型,键为Task['status'](联合类型),值为Task[]。 - 所有类型添加 JSDoc 注释说明用途。
任务要求:请写出一段完整的中文提示词,发送给 AI,使其生成符合上述要求的 TypeScript 类型定义代码。提示词中需明确指定各工具类型的组合方式、Task 接口结构以及各派生类型的推导逻辑。
三、Agent 模式下的提示词示例
你是一个资深前端开发 Agent。请创建 TypeScript 文件
src/types/task.ts,为任务管理应用设计完整的类型系统。要求:
interface User { id: number; name: string; }。interface Task { id: number; title: string; description: string; status: 'todo' | 'in-progress' | 'done'; assignee: User; tags: string[]; createdAt: Date; updatedAt: Date; }。type TaskPreview = Pick<Task, 'id' | 'title' | 'status' | 'assignee'>;——用于列表展示的预览类型。type CreateTaskInput = Omit<Task, 'id' | 'createdAt' | 'updatedAt'>;——创建任务时的输入类型,排除系统字段。type UpdateTaskInput = Partial<Omit<Task, 'id' | 'createdAt'>>;——更新任务时的输入类型,所有业务字段可选,排除不可修改字段。type TasksByStatus = Record<Task['status'], Task[]>;——按状态分组的任务映射。- 为每个类型添加 JSDoc 注释。在文件末尾添加一个简短的示例对象注释,展示各类型的使用。
- 使用 ESM 导出所有类型(
export type)。确保代码可直接编译。完成后列出完整文件内容。
四、面试真题与参考答案
题目(字节跳动前端面试题):
请解释
Pick<T, K>和Omit<T, K>的区别。在什么场景下会选择使用Pick,什么场景下会选择Omit?请举例说明它们的典型使用场景。
参考答案:
| 对比维度 | Pick<T, K> |
Omit<T, K> |
|---|---|---|
| 语义 | 选取:从 T 中挑选需要的属性子集。 |
排除:从 T 中删除不需要的属性。 |
| 结果 | 返回一个仅包含 K 中指定属性的新类型。 |
返回一个排除了 K 中指定属性的新类型。 |
| 键数量 | 通常键的数量减少(从全集中挑选少量)。 | 通常键的数量略减(从全集中删去少量)。 |
| 可读性 | 明确表达“我需要哪些字段”。 | 明确表达“我不需要哪些字段”。 |
| 实现 | { [P in K]: T[P] } |
Pick<T, Exclude<keyof T, K>> |
选择原则:
- 当需要的字段远少于不需要的字段时,使用
Pick更简洁清晰。例如从 20 个字段的 User 模型中选取 3 个公开字段,Pick<User, 'id' | 'name' | 'email'>比Omit<User, 'password' | 'age' | 'address' | ...>更直观。 - 当需要排除的字段远少于保留的字段时,使用
Omit更简洁。例如创建用户时排除id、createdAt两个系统字段,Omit<User, 'id' | 'createdAt'>非常清晰。 - 当类型经常新增字段时,
Omit更具弹性——排除固定的系统字段后,新增的业务字段会自动被包含,无需更新类型定义。
典型场景:
Pick:API 响应的字段裁剪、组件 Props 的子集继承。Omit:排除密码等敏感字段、排除系统字段(id、createdAt)、从父组件继承 Props 时排除特定属性。
课后练习答案
一、概念自测答案
B
- 解析:
Omit<T, K>通过Pick<T, Exclude<keyof T, K>>实现——先从键中排除K,再Pick剩余键。
- 解析:
B
- 解析:
Readonly<T>是浅层只读,只影响直接属性。嵌套对象内部的属性仍可被修改,需要递归DeepReadonly才能实现深层不可变。
- 解析:
Record- 解析:
Record<'a' | 'b' | 'c', boolean>构造键为'a' | 'b' | 'c',值为boolean的对象类型。
- 解析:
A、B、C
- 解析:
Partial、Pick、Record均基于映射类型实现。Exclude基于分布式条件类型,不直接使用映射类型。
- 解析:
二、AI 编程任务参考答案(提示词示例)
示例提示词:
“请为任务管理应用设计 TypeScript 类型层。要求:
interface User { id: number; name: string; }interface Task包含id, title, description, status(联合字面量), assignee(User), tags(string[]), createdAt(Date), updatedAt(Date)。type TaskPreview = Pick<Task, 'id' | 'title' | 'status' | 'assignee'>;type CreateTaskInput = Omit<Task, 'id' | 'createdAt' | 'updatedAt'>;type UpdateTaskInput = Partial<Omit<Task, 'id' | 'createdAt'>>;type TasksByStatus = Record<Task['status'], Task[]>;- 所有类型添加 JSDoc 注释。直接输出完整代码。”