内置工具类型

深入理解 TypeScript 内置工具类型的实现原理与使用场景,掌握类型编程的核心构建块。

内置工具类型

概述

TypeScript 内置了一组工具类型(Utility Types),它们是对映射类型、条件类型、keyof 等类型原语的封装。理解这些工具类型的实现原理,不仅能正确使用它们,更能为自定义类型工具打下基础。

面试定位:面试中经常要求手写 Partial、Pick 等工具类型的实现,或在给定场景中选择正确的工具类型组合。这类问题考察的是对映射类型和条件类型的掌握程度。


属性修饰类工具类型

Partial<T> — 全部变为可选

// 源码实现
type Partial<T> = {
  [P in keyof T]?: T[P];
};

// 使用场景:更新操作的参数
interface User {
  name: string;
  email: string;
  age: number;
}

function updateUser(id: string, updates: Partial<User>) {
  // updates 的每个字段都是可选的
}

updateUser("1", { name: "Alice" }); // ✅ 只更新 name

Required<T> — 全部变为必需

// 源码实现
type Required<T> = {
  [P in keyof T]-?: T[P];
};

// -? 语法移除可选修饰符

Readonly<T> — 全部变为只读

// 源码实现
type Readonly<T> = {
  readonly [P in keyof T]: T[P];
};

// 使用场景:不可变状态
function freeze<T extends object>(obj: T): Readonly<T> {
  return Object.freeze(obj);
}

注意Readonly<T> 只是浅层只读。嵌套对象的属性仍然可变。深层只读需要递归实现:

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

// 使用场景:API 响应只返回部分字段
type UserSummary = Pick<User, "name" | "email">;
// { name: string; email: string }

Omit<T, K> — 排除指定属性

// 源码实现
type Omit<T, K extends keyof any> = Pick<T, Exclude<keyof T, K>>;

// 使用场景:创建实体时排除自动生成的字段
type CreateUserInput = Omit<User, "id" | "createdAt">;

Pick vs Omit 的选择:当需要保留的属性少于排除的属性时用 Pick;反之用 Omit。更重要的是可维护性——如果原类型新增了字段:

  • Pick 不会自动包含新字段(安全但可能遗漏)
  • Omit 会自动包含新字段(便利但可能泄漏)

键值映射类工具类型

Record<K, V> — 构造键值对类型

// 源码实现
type Record<K extends keyof any, T> = {
  [P in K]: T;
};

// 使用场景一:字典/映射
type RolePermissions = Record<"admin" | "editor" | "viewer", string[]>;

// 使用场景二:索引签名的类型安全替代
const cache: Record<string, unknown> = {};

// 使用场景三:确保枚举值完整覆盖
type Status = "idle" | "loading" | "error" | "success";
const statusMessages: Record<Status, string> = {
  idle: "等待中",
  loading: "加载中",
  error: "出错了",
  success: "完成",
  // 缺少任何一个 Status 值都会报错
};

联合类型操作工具类型

Exclude<T, U> — 从联合类型中排除

// 源码实现
type Exclude<T, U> = T extends U ? never : T;

type T = Exclude<"a" | "b" | "c", "a" | "b">;
// "c"

Extract<T, U> — 从联合类型中提取

// 源码实现
type Extract<T, U> = T extends U ? T : never;

type T = Extract<string | number | boolean, string | number>;
// string | number

NonNullable<T> — 排除 null 和 undefined

// 源码实现
type NonNullable<T> = T & {};
// 早期版本:T extends null | undefined ? never : T

type T = NonNullable<string | null | undefined>;
// string

函数相关工具类型

ReturnType<T> — 提取返回值类型

// 源码实现
type ReturnType<T extends (...args: any) => any> =
  T extends (...args: any) => infer R ? R : any;

// 使用场景
function createUser() {
  return { id: "1", name: "Alice", role: "admin" as const };
}

type User = ReturnType<typeof createUser>;
// { id: string; name: string; role: "admin" }

Parameters<T> — 提取参数类型

// 源码实现
type Parameters<T extends (...args: any) => any> =
  T extends (...args: infer P) => any ? P : never;

function search(query: string, limit: number, offset: number) { /* ... */ }

type SearchParams = Parameters<typeof search>;
// [query: string, limit: number, offset: number]

ConstructorParameters<T> 与 InstanceType<T>

class UserService {
  constructor(private db: Database, private logger: Logger) {}
}

type Deps = ConstructorParameters<typeof UserService>;
// [db: Database, logger: Logger]

type Instance = InstanceType<typeof UserService>;
// UserService

Promise 相关

Awaited<T>(TypeScript 4.5+)

递归解包 Promise 类型:

// 简化的源码逻辑
type Awaited<T> = T extends Promise<infer U> ? Awaited<U> : T;

type A = Awaited<Promise<string>>;            // string
type B = Awaited<Promise<Promise<number>>>;   // number
type C = Awaited<string>;                     // string

字符串操作工具类型

TypeScript 4.1+ 引入了模板字面量类型配套的字符串操作类型:

type Upper = Uppercase<"hello">;      // "HELLO"
type Lower = Lowercase<"HELLO">;      // "hello"
type Cap = Capitalize<"hello">;       // "Hello"
type Uncap = Uncapitalize<"Hello">;   // "hello"

工具类型组合模式

实际项目中,工具类型的价值在于组合:

// 模式一:创建输入类型
type CreateInput<T> = Omit<T, "id" | "createdAt" | "updatedAt">;
type UpdateInput<T> = Partial<CreateInput<T>> & { id: string };

// 模式二:使部分属性可选
type PartialBy<T, K extends keyof T> = Omit<T, K> & Partial<Pick<T, K>>;

// 模式三:使部分属性必需
type RequiredBy<T, K extends keyof T> = Omit<T, K> & Required<Pick<T, K>>;

// 模式四:深层 Partial
type DeepPartial<T> = {
  [P in keyof T]?: T[P] extends object ? DeepPartial<T[P]> : T[P];
};

// 模式五:提取函数参数中某个位置的类型
type FirstArg<T extends (...args: any) => any> = Parameters<T>[0];

面试高频问题

Q: 请手写 Pick 的实现

回答

type MyPick<T, K extends keyof T> = {
  [P in K]: T[P];
};

关键点:K extends keyof T 确保只能选取 T 上存在的属性;[P in K] 是映射类型语法,遍历联合类型 K 的每个成员。

Q: Omit 为什么用 keyof any 而不是 keyof T

回答要点Omit<T, K> 的 K 约束为 keyof any(即 string | number | symbol)而非 keyof T,这是有意的设计选择。它允许排除 T 上不存在的键,使得 Omit 在泛型上下文中更加灵活——当 T 是泛型参数时,编译器可能无法确定 K 是否属于 keyof T

Q: Record<string, unknown> 和 object 有什么区别?

回答要点Record<string, unknown> 表示一个具有字符串索引签名的对象,所有值类型为 unknown,可以安全地进行属性访问。object 只表示"非原始类型",不提供任何属性信息,无法进行索引访问。在需要表示"任意键值对对象"时,Record<string, unknown> 是类型安全的选择。


延展阅读