Skip to content

随着我们对 TypeScript 的掌握日渐深入,我们会发现自己经常在进行一些重复性的“类型体操”:从一个现有类型中创建新类型,让所有属性变为可选,或者移除某些敏感属性。如果我们每次都手动编写这些转换逻辑,不仅会使代码变得冗长,还会增加出错的风险。

幸运的是,TypeScript 团队预见到了这些常见的需求,并为我们内置了一套功能强大的“类型工具箱”——实用工具类型 (Utility Types)

这些工具就像是类型世界的“高阶函数”,它们接收一个或多个类型作为输入,然后返回一个经过转换的新类型。它们是 TypeScript 核心特性的集大成者,巧妙地运用了泛型、条件类型和映射类型,让复杂的类型转换变得轻而易举。掌握它们,是提升开发效率和代码质量的捷径。

本文将带你深入探索四个最核心、最常用的实用工具类型:Partial, Readonly, Pick, 和 Omit

1. Partial<T>:让一切变为可选

  • 作用:构造一个新类型,并将 T 类型的所有属性设置为可选 (?)。
  • 应用场景:当你需要处理一个对象的“部分更新”时,Partial 无比实用。例如,在一个更新用户信息的函数中,用户可能只提交了需要修改的字段,而不是完整的用户对象。

示例:

typescript
interface User {
  id: number;
  username: string;
  email: string;
  bio: string;
}

// 这个函数用于更新用户信息
function updateUser(id: number, fieldsToUpdate: Partial<User>) {
  // ... 在这里执行更新逻辑
  // fieldsToUpdate 可以是 { username: 'newname' }
  // 也可以是 { email: 'new@email.com', bio: 'new bio' }
}

updateUser(1, { username: "RaycastAI" }); // 合法
updateUser(1, { email: "ai@raycast.com", bio: "Loves TS." }); // 合法

Partial<User> 自动生成了一个新类型 { id?: number; username?: string; ... },它允许我们传入 User 属性的任意子集,而无需手动为每个属性添加 ?

2. Readonly<T>:构建不可变壁垒

  • 作用:构造一个新类型,并将 T 类型的所有属性设置为只读 (readonly)。
  • 应用场景:当你希望创建一个不可变的数据对象时,Readonly 是你的首选。这在处理应用配置、或者从 API 获取并存储到状态管理中的数据时非常有用,可以防止意外的修改。

示例:

typescript
interface AppConfig {
  apiUrl: string;
  theme: 'dark' | 'light';
}

const config: Readonly<AppConfig> = {
  apiUrl: "https://api.raycast.com",
  theme: "dark",
};

// 任何修改尝试都会在编译阶段被捕获
// config.theme = "light"; 
// 编译时错误: Cannot assign to 'theme' because it is a read-only property.

Readonly<AppConfig> 确保了 config 对象在初始化后不会被篡改,为代码的稳定性增加了一道坚固的防线。

3. Pick<T, K>:精准挑选所需

  • 作用:从类型 T 中“挑选”出一组属性 KKT 中属性名的联合类型),并创建一个只包含这些属性的新类型。
  • 应用场景:当你需要从一个复杂的对象类型中,创建一个只包含部分关键信息的新类型时,Pick 非常有用。例如,从完整的用户对象中创建一个用于列表展示的“用户预览”类型。

示例:

typescript
interface User {
  id: number;
  username: string;
  email: string;
  createdAt: Date;
  lastLoginAt: Date;
}

// 创建一个只包含 id 和 username 的用户预览类型
type UserPreview = Pick<User, "id" | "username">;

// UserPreview 等价于:
// {
//   id: number;
//   username: string;
// }

const userForList: UserPreview = {
  id: 1,
  username: "RaycastAI",
};

Pick 让我们能够以声明式的方式,精确地从现有类型中派生出更小、更专注的子类型,极大地增强了代码的可读性和复用性。

4. Omit<T, K>:优雅地排除多余

  • 作用:与 Pick 相反,Omit 从类型 T 中构造一个新类型,该类型拥有 T 的所有属性,但排除了指定的属性集 K
  • 应用场景:当你需要从一个类型中移除敏感信息或不需要的字段时,Omit 是最佳选择。例如,在将数据库中的用户对象返回给前端时,移除 password 字段。

示例:

typescript
interface UserWithPassword {
  id: number;
  username: string;
  email: string;
  passwordHash: string;
}

// 创建一个不包含 passwordHash 的公共用户类型
type PublicUser = Omit<UserWithPassword, "passwordHash">;

// PublicUser 等价于:
// {
//   id: number;
//   username:string;
//   email: string;
// }

function sendUserToClient(user: UserWithPassword): PublicUser {
  const { passwordHash, ...publicData } = user;
  return publicData;
}

Omit 提供了一种非常清晰的方式来表达“我需要除了这些之外的所有属性”,这在处理数据转换和 API 边界时尤其有用。

总结

实用工具类型是 TypeScript 赋予我们的高效“快捷方式”。它们封装了常见的类型转换逻辑,让我们能够以更少的代码,做更多的事情,同时保持类型系统的严谨性。

  • Partial<T>:用于部分更新,让所有属性可选。
  • Readonly<T>:用于创建不可变数据,让所有属性只读。
  • Pick<T, K>:用于创建子集类型,只保留指定属性。
  • Omit<T, K>:用于创建“净化”后的类型,排除指定属性。

这四个只是冰山一角。TypeScript 还提供了 Required<T>, Record<K,T>, Exclude<T,U>, ReturnType<T> 等一系列强大的工具。当你发现自己正在手动进行复杂的类型转换时,不妨先查阅一下 TypeScript 的官方文档——很可能已经有一个现成的实用工具类型在等着你了。

不知道说啥了很无语了