第 7 章

内置工具类型源码拆解:Partial/Pick/Omit/Record

第7章:内置工具类型源码拆解:Partial/Pick/Omit/Record

理解内置工具类型源码拆解是掌握 TypeScript 类型系统的关键一步。

本章核心问题:如何在实际项目中正确使用Partial/Pick/Omit/Record?关键的设计决策和陷阱是什么?

读完本章你将理解


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];
};

拆解:

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];
};

拆解:

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];
};

拆解:

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>>;

拆解:

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;
};

拆解:

// 基础用法:查找表
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;

拆解:

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> 不是原生映射类型,而是 PickExclude 的组合——注意 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 关键字——从任意类型中提取子类型的完整技术。

本章评分
4.6  / 5  (50 评分)

💬 留言讨论