WinddSnow

TypeScript-Built-in-Utility-Types-Part1-Partial-Required-Readonly-Pick-Omit-R...

字数统计: 4.2k阅读时长: 18 min
2026/07/31

第71课:内置工具类型(上)——Partial/Required/Readonly/Pick/Omit/Record

TypeScript 提供了二十余个内置工具类型(Utility Types),它们本质上是预先编写好的泛型类型,用于对现有类型进行变换——添加或移除修饰符、选取或排除属性、构造新的对象形状。理解这些工具类型的实现原理,不仅能让你更高效地使用它们,还能帮助你深入掌握映射类型、条件类型和 keyof 的配合运用。本节课将逐源码级拆解最常用的六个工具类型:PartialRequiredReadonlyPickOmitRecord,涵盖它们的定义、行为、使用场景和常见陷阱。


1. 属性修饰工具:PartialRequiredReadonly

这三个工具类型通过映射类型修饰符来改变对象属性的可选性和可变性。它们的源码来自 TypeScript 的 lib.es5.d.ts

1.1 Partial<T>:所有属性变为可选

Partial<T> 将类型 T 的所有属性变为可选

源码级实现

1
2
3
type Partial<T> = {
[P in keyof T]?: T[P];
};

[P in keyof T]?: T[P] 遍历 T 的所有键,为每个属性添加 ? 修饰符,使其成为可选属性。属性值的类型保持不变(T[P])。

1
2
3
4
5
6
7
8
9
10
11
12
interface User {
id: number;
name: string;
email: string;
age: number;
}

// 所有属性变为可选
type PartialUser = Partial<User>;
// { id?: number; name?: string; email?: string; age?: number; }

const update: PartialUser = { name: 'Alice' }; // ✅ 仅传入部分字段

典型场景

  • 表单部分更新:用户修改资料时,只提交被修改的字段。
  • 配置合并:将用户配置与默认配置合并时,用户配置可以是 Partial<Config>
  • 对象构建器:逐步构造一个复杂对象,构造过程中允许属性缺失。
1
2
3
4
5
6
7
function updateUser(id: number, changes: Partial<User>): void {
// changes 中只需要包含要修改的字段
console.log(`Updating user ${id} with`, changes);
}

updateUser(1, { name: 'Bob' }); // ✅
updateUser(2, { name: 'Carol', age: 30 }); // ✅

1.2 Required<T>:所有属性变为必填

Required<T> 将类型 T 的所有属性变为必填(移除可选修饰符)。

源码级实现

1
2
3
type Required<T> = {
[P in keyof T]-?: T[P];
};

-? 是映射类型中的修饰符移除语法:显式移除 ? 可选修饰符。+? 添加可选,-? 移除可选。

1
2
3
4
5
6
7
8
9
10
11
interface Config {
url?: string;
timeout?: number;
retries?: number;
}

type FullConfig = Required<Config>;
// { url: string; timeout: number; retries: number; }

// const missing: FullConfig = { url: 'http://...' }; // ❌ 缺少 timeout 和 retries
const complete: FullConfig = { url: 'http://...', timeout: 5000, retries: 3 };

典型场景

  • 严格配置校验:在函数内部,将可选配置转为必填,确保所有字段都已填充默认值。
  • 构造函数参数:某些构造阶段需要确保所有依赖已就绪。

1.3 Readonly<T>:所有属性变为只读

Readonly<T> 将类型 T 的所有属性变为只读readonly)。

源码级实现

1
2
3
type Readonly<T> = {
readonly [P in keyof T]: T[P];
};
1
2
3
4
5
6
7
interface Point {
x: number;
y: number;
}

const p: Readonly<Point> = { x: 10, y: 20 };
// p.x = 30; // ❌ 错误:Cannot assign to 'x' because it is a read-only property.

典型场景

  • 不可变数据:在函数式编程和 Redux 中,确保状态不可直接修改。
  • 冻结对象:配合 Object.freeze() 使用,类型层面也保证不可变。
  • API 响应:从后端获取的数据通常不应被修改,用 Readonly<T> 包装。

重要限制Readonly<T>浅层的——只读仅作用于第一层属性。嵌套对象内部的属性仍可修改。

1
2
3
4
5
6
7
8
9
10
11
12
interface User {
name: string;
profile: { bio: string; avatar: string };
}

const u: Readonly<User> = {
name: 'Alice',
profile: { bio: 'Developer', avatar: 'url' }
};

// u.name = 'Bob'; // ❌ 错误:第一层只读
u.profile.bio = 'Designer'; // ✅ 允许!嵌套属性未被 Readonly 影响

如需深层只读,需要使用 DeepReadonly<T>(第 70 课已实现)或第三方库的类型。


2. 结构构造工具:PickOmitRecord

这三个工具类型用于构造新的对象形状——从已有类型中选取子集、排除子集,或从零开始构建键值对映射。

2.1 Pick<T, K>:选取属性子集

Pick<T, K> 从类型 T选取指定键 K 构成的类型。K 必须是 T 的键的联合子类型。

源码级实现

1
2
3
type Pick<T, K extends keyof T> = {
[P in K]: T[P];
};
  • K extends keyof T 约束了 K 必须是 T 的属性键的子集。
  • [P in K]: T[P] 遍历 K 中的每个键,从 T 中获取其值类型。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
interface User {
id: number;
name: string;
email: string;
password: string;
age: number;
}

// 从 User 中提取公开信息(不含密码)
type PublicUser = Pick<User, 'id' | 'name' | 'email'>;
// { id: number; name: string; email: string; }

// 非法——'address' 不是 User 的属性键
// type Invalid = Pick<User, 'name' | 'address'>; // ❌

典型场景

  • 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
2
3
4
5
6
7
8
9
10
11
12
13
14
interface User {
id: number;
name: string;
email: string;
password: string;
}

// 排除 password 字段
type UserWithoutPassword = Omit<User, 'password'>;
// { id: number; name: string; email: string; }

// 排除多个字段
type UserPreview = Omit<User, 'password' | 'email'>;
// { id: number; name: string; }

典型场景

  • 脱敏处理:从数据中排除密码、Token 等敏感字段。
  • Props 转发:将父组件的 Props 排除掉子组件不需要的属性后转发。
  • 表单差异:编辑表单缺少 idcreatedAt 等系统字段。

2.3 Record<K, V>:构造键值对对象

Record<K, V> 构造一个对象类型,其键的类型为 K,值的类型为 V。它是索引签名的泛型封装。

源码级实现

1
2
3
type Record<K extends keyof any, V> = {
[P in K]: V;
};
  • K extends keyof any 约束 K 必须是合法的键类型(string | number | symbol)。
  • 映射类型 [P in K]: V 遍历 K 的每个键,为每个键赋予相同的值类型 V
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
// 字典:字符串键 → 字符串值
type Dictionary = Record<string, string>;
const dict: Dictionary = { hello: 'world', foo: 'bar' };

// 页面映射:已知键集合 → 页面信息
type PageInfo = Record<'home' | 'about' | 'contact', { title: string; url: string }>;

const pages: PageInfo = {
home: { title: '首页', url: '/' },
about: { title: '关于我们', url: '/about' },
contact: { title: '联系我们', url: '/contact' },
};

// 枚举值映射
type HttpStatus = 200 | 301 | 404 | 500;
type StatusMessage = Record<HttpStatus, string>;

const messages: StatusMessage = {
200: 'OK',
301: 'Moved Permanently',
404: 'Not Found',
500: 'Internal Server Error',
};

典型场景

  • 字典/映射表:键值对集合,如翻译词典、配置表。
  • 枚举映射:将状态码或枚举值映射到对应的显示文本。
  • 缓存存储Record<string, CacheEntry>

2.4 PickOmitRecord 的对比

工具类型 输入 输出 逻辑
Pick<T, K> T(源类型)+ K(键) 仅包含 K 中键的新对象类型 选取子集,从多到少
Omit<T, K> T(源类型)+ K(键) 排除 K 中键的新对象类型 删除部分,从多到少
Record<K, V> K(键)+ V(值类型) 全新的对象类型 从零构建,给定键和值

3. 综合实战:API 数据层类型设计

以下示例综合运用 PartialPickOmitRecord,构建一个类型安全的 API 数据访问层。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
// 完整的数据库模型
interface User {
id: number;
name: string;
email: string;
password: string;
role: 'admin' | 'user';
createdAt: Date;
updatedAt: Date;
}

// 创建用户时的输入(无 id、createdAt、updatedAt)
type CreateUserInput = Omit<User, 'id' | 'createdAt' | 'updatedAt'>;
// { name: string; email: string; password: string; role: 'admin' | 'user'; }

// 更新用户时的输入(所有字段可选,排除 id)
type UpdateUserInput = Partial<Omit<User, 'id' | 'createdAt'>>;
// { name?: string; email?: string; password?: string; role?: 'admin' | 'user'; updatedAt?: Date; }

// 返回给客户端的用户信息(排除密码)
type SafeUser = Omit<User, 'password'>;
// { id: number; name: string; email: string; role: 'admin' | 'user'; createdAt: Date; updatedAt: Date; }

// 用户列表的 API 响应包装
type UserListResponse = Record<'users', SafeUser[]> & { total: number };
// { users: SafeUser[]; total: number; }

// 使用示例
function createUser(input: CreateUserInput): SafeUser {
// ...存储逻辑
return { id: 1, ...input, createdAt: new Date(), updatedAt: new Date() };
}

function updateUser(id: number, changes: UpdateUserInput): SafeUser {
// ...更新逻辑
return { id, name: 'Alice', email: 'alice@example.com', role: 'user', createdAt: new Date(), updatedAt: new Date() };
}

设计要点

  • Omit 从完整模型中剥离系统字段(idcreatedAt)得到 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 会忽略它)。这是一把双刃剑——提供了灵活性,但也可能掩盖拼写错误。如果需要严格约束,可在包装类型中显式检查。


课后练习

一、概念自测(选择题 / 填空题)

  1. (单选) 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>>

  2. (单选) Readonly<T> 只读效果的“浅层”指的是什么?
    A. 只能读取对象的原型属性。
    B. 仅第一层属性变为只读,嵌套对象内部属性仍可修改。
    C. 只能在函数内部使用。
    D. 只读效果仅作用于数组元素。

  3. (填空) 要构造一个键为 'a' | 'b' | 'c',值类型均为 boolean 的对象类型,应使用 ______<'a' | 'b' | 'c', boolean>

  4. (多选) 以下哪些工具类型使用了映射类型作为底层实现?
    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')、assigneeUser 接口)、tags(string 数组)、createdAt(Date)、updatedAt(Date)。
  • 使用 Pick 定义 TaskPreview 类型,仅包含 idtitlestatusassignee
  • 使用 Omit 定义 CreateTaskInput 类型,排除 idcreatedAtupdatedAt
  • 使用 Partial 结合 Omit 定义 UpdateTaskInput 类型,所有业务字段可选,且排除 idcreatedAt
  • 使用 Record 定义 TasksByStatus 类型,键为 Task['status'](联合类型),值为 Task[]
  • 所有类型添加 JSDoc 注释说明用途。

任务要求:请写出一段完整的中文提示词,发送给 AI,使其生成符合上述要求的 TypeScript 类型定义代码。提示词中需明确指定各工具类型的组合方式、Task 接口结构以及各派生类型的推导逻辑。

三、Agent 模式下的提示词示例

你是一个资深前端开发 Agent。请创建 TypeScript 文件 src/types/task.ts,为任务管理应用设计完整的类型系统。要求:

  1. interface User { id: number; name: string; }
  2. interface Task { id: number; title: string; description: string; status: 'todo' | 'in-progress' | 'done'; assignee: User; tags: string[]; createdAt: Date; updatedAt: Date; }
  3. type TaskPreview = Pick<Task, 'id' | 'title' | 'status' | 'assignee'>;——用于列表展示的预览类型。
  4. type CreateTaskInput = Omit<Task, 'id' | 'createdAt' | 'updatedAt'>;——创建任务时的输入类型,排除系统字段。
  5. type UpdateTaskInput = Partial<Omit<Task, 'id' | 'createdAt'>>;——更新任务时的输入类型,所有业务字段可选,排除不可修改字段。
  6. type TasksByStatus = Record<Task['status'], Task[]>;——按状态分组的任务映射。
  7. 为每个类型添加 JSDoc 注释。在文件末尾添加一个简短的示例对象注释,展示各类型的使用。
  8. 使用 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 更简洁。例如创建用户时排除 idcreatedAt 两个系统字段,Omit<User, 'id' | 'createdAt'> 非常清晰。
  • 当类型经常新增字段时,Omit 更具弹性——排除固定的系统字段后,新增的业务字段会自动被包含,无需更新类型定义。

典型场景

  • Pick:API 响应的字段裁剪、组件 Props 的子集继承。
  • Omit:排除密码等敏感字段、排除系统字段(id、createdAt)、从父组件继承 Props 时排除特定属性。

课后练习答案

一、概念自测答案

  1. B

    • 解析:Omit<T, K> 通过 Pick<T, Exclude<keyof T, K>> 实现——先从键中排除 K,再 Pick 剩余键。
  2. B

    • 解析:Readonly<T> 是浅层只读,只影响直接属性。嵌套对象内部的属性仍可被修改,需要递归 DeepReadonly 才能实现深层不可变。
  3. Record

    • 解析:Record<'a' | 'b' | 'c', boolean> 构造键为 'a' | 'b' | 'c',值为 boolean 的对象类型。
  4. A、B、C

    • 解析:PartialPickRecord 均基于映射类型实现。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 注释。直接输出完整代码。”
CATALOG
  1. 1. 第71课:内置工具类型(上)——Partial/Required/Readonly/Pick/Omit/Record
    1. 1.1. 1. 属性修饰工具:Partial、Required、Readonly
      1. 1.1.1. 1.1 Partial<T>:所有属性变为可选
      2. 1.1.2. 1.2 Required<T>:所有属性变为必填
      3. 1.1.3. 1.3 Readonly<T>:所有属性变为只读
    2. 1.2. 2. 结构构造工具:Pick、Omit、Record
      1. 1.2.1. 2.1 Pick<T, K>:选取属性子集
      2. 1.2.2. 2.2 Omit<T, K>:排除属性子集
      3. 1.2.3. 2.3 Record<K, V>:构造键值对对象
      4. 1.2.4. 2.4 Pick、Omit 与 Record 的对比
    3. 1.3. 3. 综合实战:API 数据层类型设计
    4. 1.4. 4. 最佳实践与常见陷阱
      1. 1.4.1. 4.1 Partial 的误用:当你需要全部字段时
      2. 1.4.2. 4.2 Readonly 的浅层限制
      3. 1.4.3. 4.3 Record 的键兼容性
      4. 1.4.4. 4.4 Omit 宽松的键约束
    5. 1.5. 课后练习
      1. 1.5.1. 一、概念自测(选择题 / 填空题)
      2. 1.5.2. 二、AI 编程任务:编写面向 AI 的提示词
      3. 1.5.3. 三、Agent 模式下的提示词示例
      4. 1.5.4. 四、面试真题与参考答案
    6. 1.6. 课后练习答案
      1. 1.6.1. 一、概念自测答案
      2. 1.6.2. 二、AI 编程任务参考答案(提示词示例)