内置工具类型源码拆解:Partial/Pick/Omit/Record
第7章:内置工具类型源码拆解:Partial/Pick/Omit/Record
理解内置工具类型源码拆解是掌握 TypeScript 类型系统的关键一步。
本章核心问题:如何在实际项目中正确使用Partial/Pick/Omit/Record?关键的设计决策和陷阱是什么?
读完本章你将理解:
- 阅读工具类型源码的意义
Partial<T>— 所有字段变可选Required<T>— 所有字段变必填
Level 1 · 你需要知道的(1-3年经验)
阅读工具类型源码的意义
TypeScript 内置的工具类型都定义在 lib.es5.d.ts 中,源码公开、可直接阅读。读懂它们的意义不只是"知道怎么用",而是理解映射类型、条件类型的组合方式,从而能写出自己的工具类型。
打开终端,找到 TypeScript 安装目录:
# 查看 TypeScript 内置类型定义
node_modules/typescript/lib/lib.es5.d.ts
# 或者直接 Cmd+Click 任意工具类型名称,IDE 会跳转到源码
Partial<T> — 所有字段变可选
源码:
type Partial<T> = {
[P in keyof T]?: T[P];
};
拆解:
keyof T:获取 T 的所有键名,形成联合类型[P in keyof T]:映射类型,遍历每一个键 P?::给每个键加上可选修饰符T[P]:保持原来的值类型不变
interface User {
id: number;
name: string;
email: string;
role: "admin" | "user";
}
// Partial<User> 等价于:
// {
// id?: number;
// name?: string;
// email?: string;
// role?: "admin" | "user";
// }
// 典型用途:update 函数只传需要修改的字段
function updateUser(id: number, patch: Partial<User>): User {
const existing = findUserById(id); // 假设已有的获取函数
return { ...existing, ...patch };
}
updateUser(1, { name: "Bob" }); // 只更新 name
updateUser(1, { email: "[email protected]", role: "admin" }); // 更新多个字段
updateUser(1, { id: 999 }); // 合法但不推荐,可用 Omit<Partial<User>, "id">
深度 Partial(标准库不提供,需自己实现):
// 标准 Partial 只处理一层
interface DeepNested {
a: {
b: {
c: string;
};
};
}
// 标准 Partial<DeepNested> 的 a 变为可选,
// 但 a.b 和 a.b.c 仍然是必填
type StdPartial = Partial<DeepNested>;
// { a?: { b: { c: string } } }
// 深度 Partial 递归处理所有层级
type DeepPartial<T> = {
[P in keyof T]?: T[P] extends object ? DeepPartial<T[P]> : T[P];
};
Required<T> — 所有字段变必填
源码:
type Required<T> = {
[P in keyof T]-?: T[P];
};
拆解:
-?:减号修饰符,移除可选标记。对应的+?是加上可选(通常省略+)
interface FormData {
name?: string;
email?: string;
age?: number;
}
// Required<FormData> 等价于:
// { name: string; email: string; age: number }
// 典型用途:表单校验通过后,类型从"可能存在"变为"确定存在"
function validateForm(data: FormData): Required<FormData> {
if (!data.name) throw new Error("name required");
if (!data.email) throw new Error("email required");
if (!data.age) throw new Error("age required");
return data as Required<FormData>; // 校验后断言
}
function submitForm(data: Required<FormData>) {
// data.name.toUpperCase() — 安全,name 一定存在
console.log(data.name.toUpperCase());
}
Readonly<T> — 所有字段变只读
源码:
type Readonly<T> = {
readonly [P in keyof T]: T[P];
};
拆解:
readonly:加上只读修饰符,赋值会在编译期报错
interface Config {
apiUrl: string;
timeout: number;
retries: number;
}
const config: Readonly<Config> = {
apiUrl: "https://api.example.com",
timeout: 5000,
retries: 3,
};
config.timeout = 10000; // Error: Cannot assign to 'timeout' because it is a read-only property
// 典型用途:应用配置、常量对象
// Readonly 只保护当前层,不递归
const nested: Readonly<{ inner: { x: number } }> = { inner: { x: 1 } };
nested.inner = { x: 2 }; // Error:inner 是只读的
nested.inner.x = 99; // OK:Readonly 不深入内层
// 深度只读类型
type DeepReadonly<T> = {
readonly [P in keyof T]: T[P] extends object ? DeepReadonly<T[P]> : T[P];
};
Pick<T, K> — 挑选指定字段
源码:
type Pick<T, K extends keyof T> = {
[P in K]: T[P];
};
拆解:
K extends keyof T:K 必须是 T 的键名的子集[P in K]:只遍历 K 中的键T[P]:保持原来的值类型
interface User {
id: number;
name: string;
email: string;
password: string;
role: "admin" | "user";
createdAt: Date;
}
// 只挑选公开字段
type PublicUser = Pick<User, "id" | "name" | "email">;
// { id: number; name: string; email: string }
// 典型用途:DTO(数据传输对象)模式
// API 返回给前端的数据不应包含 password
function getUserProfile(id: number): PublicUser {
const user = findUserById(id);
return {
id: user.id,
name: user.name,
email: user.email,
};
}
// 动态字段选择
function selectFields<T, K extends keyof T>(obj: T, keys: K[]): Pick<T, K> {
const result = {} as Pick<T, K>;
keys.forEach(k => { result[k] = obj[k]; });
return result;
}
const summary = selectFields(user, ["id", "name"]);
// 类型:Pick<User, "id" | "name">
Level 2 · 它是怎么运行的(3-5年经验)
Omit<T, K> — 排除指定字段
源码(基于 Pick + Exclude 组合实现):
type Omit<T, K extends keyof any> = Pick<T, Exclude<keyof T, K>>;
拆解:
Exclude<keyof T, K>:从 T 的所有键中排除 K,得到剩余键的联合类型- 再用 Pick 挑选剩余键 — Omit 不是原生映射类型,而是两个工具类型的组合
interface User {
id: number;
name: string;
email: string;
password: string;
role: "admin" | "user";
}
// 排除 password 字段
type SafeUser = Omit<User, "password">;
// { id: number; name: string; email: string; role: "admin" | "user" }
// 排除多个字段
type CreateUserDto = Omit<User, "id" | "createdAt">;
// 创建用户时不需要传 id(由服务端生成)
// 反模式:Omit vs Pick 如何选择
// - 要包含少数几个字段 → Pick(明确说"要什么")
// - 要排除少数几个字段 → Omit(明确说"不要什么")
// - 字段很多,只排除 1-2 个 → Omit 更方便维护
// 注意 Omit 的一个边界行为
interface Base {
a: string;
b: number;
}
type Weird = Omit<Base, "c">; // 不报错!"c" 不在 keyof Base 中也不会报错
// 这是因为 K extends keyof any,而不是 K extends keyof T
// 更严格的版本:
type StrictOmit<T, K extends keyof T> = Pick<T, Exclude<keyof T, K>>;
type Strict = StrictOmit<Base, "c">; // Error:'c' 不在 keyof Base 中
Record<K, V> — 构建键值映射类型
源码:
type Record<K extends keyof any, T> = {
[P in K]: T;
};
拆解:
K extends keyof any:K 必须是合法的对象键类型(string | number | symbol)[P in K]: T:每个键都对应类型 T
// 基础用法:查找表
type CountryCode = "US" | "CN" | "JP" | "DE";
type CountryName = Record<CountryCode, string>;
const countries: CountryName = {
US: "United States",
CN: "China",
JP: "Japan",
DE: "Germany",
};
// 典型用途:HTTP 状态码映射
const httpMessages: Record<number, string> = {
200: "OK",
201: "Created",
400: "Bad Request",
401: "Unauthorized",
404: "Not Found",
500: "Internal Server Error",
};
// 缓存 / 字典
type Cache<T> = Record<string, T>;
const userCache: Cache<User> = {};
function getUser(id: string): User | undefined {
return userCache[id];
}
// 和 Partial 组合:可选的查找表
type PartialRecord<K extends keyof any, V> = Partial<Record<K, V>>;
type RolePermissions = PartialRecord<
"read" | "write" | "delete",
boolean
>;
const adminPerms: RolePermissions = { read: true, write: true }; // delete 可省略
Exclude<T, U> 和 Extract<T, U> — 过滤联合成员
源码:
type Exclude<T, U> = T extends U ? never : T;
type Extract<T, U> = T extends U ? T : never;
拆解:
- 这两个是条件类型(下一章详解)
T extends U ? never : T:如果 T 的成员能赋值给 U,就过滤掉(返回 never),否则保留never在联合类型中会被自动移除:string | never = string
type Status = "pending" | "active" | "suspended" | "deleted";
// 排除某些成员
type ActiveStatus = Exclude<Status, "deleted" | "suspended">;
// "pending" | "active"
// 只保留某些成员
type TerminalStatus = Extract<Status, "suspended" | "deleted">;
// "suspended" | "deleted"
// 实际用途:过滤掉函数类型
type NonFunction<T> = Exclude<T, Function>;
type Primitives = NonFunction<string | number | (() => void)>;
// string | number
// Omit 的内部就使用了 Exclude:
// Omit<T, K> = Pick<T, Exclude<keyof T, K>>
NonNullable<T> — 去除 null 和 undefined
源码:
type NonNullable<T> = T & {};
// 旧版实现(TS 4.8 之前):
// type NonNullable<T> = T extends null | undefined ? never : T;
type MaybeString = string | null | undefined;
type DefinitelyString = NonNullable<MaybeString>; // string
// 典型用途:从可能为空的类型中提取非空版本
interface ApiResponse {
data: User | null;
error: string | null;
}
function assertData(res: ApiResponse): NonNullable<ApiResponse["data"]> {
if (!res.data) throw new Error(res.error ?? "No data");
return res.data; // 类型自动收窄为 User
}
ReturnType<T>、Parameters<T>、InstanceType<T> — 提取函数和类的类型信息
源码:
type ReturnType<T extends (...args: any) => any> =
T extends (...args: any) => infer R ? R : any;
type Parameters<T extends (...args: any) => any> =
T extends (...args: infer P) => any ? P : never;
type InstanceType<T extends abstract new (...args: any) => any> =
T extends abstract new (...args: any) => infer R ? R : any;
拆解: 这三个都用到了 infer(下一章专门讲),这里先看用法:
// ReturnType:当你无法直接导入返回值类型时
function createStore() {
return {
state: { count: 0, user: null as User | null },
dispatch: (action: string) => { /* ... */ },
getState: () => ({ count: 0 }),
};
}
type Store = ReturnType<typeof createStore>;
// { state: { count: number; user: User | null }; dispatch: (action: string) => void; ... }
// Parameters:获取函数参数类型
function login(username: string, password: string, remember: boolean): void {}
type LoginParams = Parameters<typeof login>;
// [username: string, password: string, remember: boolean]
type FirstParam = Parameters<typeof login>[0]; // string
// InstanceType:获取构造函数实例类型
class EventEmitter {
on(event: string, handler: Function): void {}
emit(event: string, ...args: unknown[]): void {}
}
type Emitter = InstanceType<typeof EventEmitter>;
// EventEmitter — 等价于直接写 EventEmitter,但在处理抽象类、Mixin 时更有用
实际模式汇总
表单局部更新
interface UserProfile {
id: number;
name: string;
bio: string;
avatar: string;
website: string;
}
// 创建:不需要 id(服务端生成)
type CreateProfileDto = Omit<UserProfile, "id">;
// 更新:id 必填,其他字段可选
type UpdateProfileDto = Pick<UserProfile, "id"> & Partial<Omit<UserProfile, "id">>;
async function updateProfile(dto: UpdateProfileDto): Promise<UserProfile> {
const current = await fetchProfile(dto.id);
return { ...current, ...dto };
}
// 使用:只传要改的字段
await updateProfile({ id: 1, bio: "New bio" });
await updateProfile({ id: 1, avatar: "new-avatar.png", website: "https://example.com" });
API 响应 DTO — 隐藏敏感字段
interface UserRecord {
id: number;
name: string;
email: string;
passwordHash: string;
salt: string;
loginAttempts: number;
lastLoginIp: string;
}
// 对外暴露的字段
type UserDto = Pick<UserRecord, "id" | "name" | "email">;
// 管理员看到更多,但还是隐藏密码相关
type AdminUserDto = Omit<UserRecord, "passwordHash" | "salt">;
配置合并
interface ServerConfig {
host: string;
port: number;
https: boolean;
timeout: number;
maxConnections: number;
}
const defaults: Readonly<ServerConfig> = {
host: "0.0.0.0",
port: 3000,
https: false,
timeout: 30000,
maxConnections: 100,
};
function createServer(options: Partial<ServerConfig>): ServerConfig {
return { ...defaults, ...options };
}
const server = createServer({ port: 8080, https: true });
// 其他字段使用默认值
Level 3 · 规范怎么定义的(资深)
TypeScript 的内置工具类型全部定义在 lib.es5.d.ts 中,它们的实现揭示了映射类型(Mapped Types)和条件类型(Conditional Types)的组合方式。Partial<T> 使用 [P in keyof T]?: 语法,其中 ? 是修饰符操作;Required<T> 使用 -? 移除可选标记,这是 TypeScript 2.8 引入的修饰符减法。Omit<T, K> 不是原生映射类型,而是 Pick 和 Exclude 的组合——注意 K extends keyof any(而非 keyof T),这意味着你可以 Omit 一个不存在的键而不报错,这是一个设计上的有意宽松。如需严格版本,可以自定义 StrictOmit<T, K extends keyof T>。
Level 4 · 边界与陷阱(所有人)
反模式:重复实现已有工具类型
// 反模式:手动实现 Partial
interface MyPartialUser {
id?: number;
name?: string;
email?: string;
}
// 每次 User 接口加字段,这里都要手动同步 — 维护噩梦
// 正确:用 Partial<User>,永远自动同步
type UpdatePayload = Partial<User>;
// 反模式:手动实现 Pick
interface UserSummary {
id: number;
name: string;
}
// 同样有同步问题
// 正确:
type UserSummary = Pick<User, "id" | "name">;
// 反模式:用类型断言绕过工具类型
const safeUser = user as { id: number; name: string };
// 断言没有类型检查,User 的 email 字段改名后这里不会报错
// 正确:
const safeUser: Pick<User, "id" | "name"> = { id: user.id, name: user.name };
总结
| 工具类型 | 源码核心 | 典型用途 |
|---|---|---|
Partial<T> |
[P in keyof T]? |
update/patch 函数参数 |
Required<T> |
[P in keyof T]-? |
校验后的数据类型 |
Readonly<T> |
readonly [P in keyof T] |
配置对象、常量 |
Pick<T, K> |
[P in K]: T[P] |
DTO、只暴露部分字段 |
Omit<T, K> |
Pick<T, Exclude<keyof T, K>> |
隐藏敏感字段 |
Record<K, V> |
[P in K]: V |
查找表、缓存、字典 |
Exclude<T, U> |
T extends U ? never : T |
过滤联合类型成员 |
Extract<T, U> |
T extends U ? T : never |
保留联合类型成员 |
NonNullable<T> |
T & {} |
去除 null/undefined |
ReturnType<T> |
infer R |
推断函数返回类型 |
Parameters<T> |
infer P |
推断函数参数类型 |
下一章:条件类型与 infer 关键字——从任意类型中提取子类型的完整技术。