TypeScript 工具类型实战:Pick、Omit、Partial、Record、Exclude

3 阅读4分钟

TypeScript 工具类型实战:Pick、Omit、Partial、Record、Exclude

在大型 TypeScript 项目中,我们经常会遇到这样的需求:从一个已有的类型里,挑出部分字段、剔除部分字段、或者把字段都变成可选。如果每次都手写一个新 interface,不仅重复劳动,而且一旦源类型改了,派生类型也要跟着改,维护成本很高。

TypeScript 内置了一套「工具类型」(Utility Types),专门用来做这类类型的变形和裁剪。本文以一段实际代码为线索,把这些工具类型的用法和它们之间的关系讲清楚。

0. 一个统一的源类型

下面所有例子都围绕同一个 User 接口展开:

interface User {
    id: number;
    name: string;
    age: number;
    email: string;
}

1. Pick<T, K>:挑选字段,形成新类型

Pick 从类型 T挑选你需要的字段,组成一个新的类型。

有什么特性?从一个类型挑选一些你需要的字段,形成新的类型。大型项目里类型消费较多,这种场景很常见。

type UserPreview = Pick<User, 'id' | 'name'>

const u: UserPreview = {
    id: 1,
    name: 'c三',
}

这里 UserPreview 只保留了 idnameageemail 都不允许出现。适合「列表项只需要展示部分字段」的场景,比如卡片预览、下拉选项等。

2. Omit<T, K>:排除字段,形成新类型

OmitPick 正好相反,它是从类型 T排除指定的字段,保留剩下的。

type UserSafe = Omit<User, 'email' | 'age'>

const safe: UserSafe = {
    id: 2,
    name: 'a',
}

UserSafe 去掉了 emailage,只剩下 idname。典型场景是「脱敏」:把一个内部类型去掉敏感字段(比如 passwordemail),返回给前端。

3. OmitPick 的等价关系

readme 里点出了最关键的一层关系:

Omit<T, K> 等价于 Pick<T, Exclude<keyof T, K>>

拆开理解:

  • keyof T:拿到 T 所有键的联合类型;
  • Exclude<keyof T, K>:从所有键里排除掉 K 中的键,保留剩下的;
  • Pick<T, ...>:再把这些剩下的键挑出来。

用代码验证一下(2.ts 就是这个思路的手工展开版):

type UserKeys = keyof User;                          // 'id' | 'name' | 'age' | 'email'
type KeepKeys = Exclude<UserKeys, 'email'>;          // 'id' | 'name' | 'age'
type MyOmitUser = Pick<User, KeepKeys>;              // 等价于 Omit<User, 'email'>

所以 Omit 本质上是 Pick + Exclude 的组合,理解了这一点,这几个工具类型就能串起来了。

4. Exclude<T, U>:从联合类型里剔除

Exclude 处理的是联合类型(union),而不是对象接口。注意和 Omit 的分工:

  • Omit 处理对象接口,删除的是接口里的字段;
  • Exclude 处理联合类型,剔除的是联合类型里的某个成员。
type All = 'id' | 'name' | 'age' | 'email';
type AfterExclude = Exclude<All, 'email'>;   // 'id' | 'name' | 'age'

5. Partial<T>:把所有属性变成可选

Partial 把类型 T 的所有属性都设置为可选

type PartialUser = Partial<User>

// patch 修改 —— 只传需要改的字段即可
const pa: PartialUser = {
    id: 3,
    name: 'b',
}

// 甚至可以传空对象
const emptyObj: PartialUser = {}

Partial 最典型的场景就是「局部更新」(patch / PATCH 接口):更新用户时只传要修改的字段,其它字段不用带上。因为所有属性都可选,传空对象也是合法的。

6. Record<K, V>:键值对类型

Record 用来描述一个「键类型 → 值类型」的映射,语法是 Record<键类型, 值类型>

type Dict = Record<string, number>;
const obj: Dict = { a: 1, b: 2 };

更实用的例子是错误码映射表:

type ErrorMsgMap = Record<number, string>;
const errorMessage: ErrorMsgMap = {
    400: '请求参数错误',
    401: '未登录,请重新登录',
    403: '没有权限访问',
    404: '资源未找到',
    500: '服务器错误',   // 500 状态码 http status code
}

function getErrorMsg(code: number): string {
    return errorMessage[code] ?? '未知错误'
}

这里 code 是 HTTP 状态码(数字),值是对应的错误文案(字符串),Record<number, string> 完美表达了这种「字典」结构。

7. ReturnType<T>:推导函数返回值类型

ReturnType 用来拿到某个函数的返回值类型,不需要手动声明:

function fn() { return { x: 1, y: 2 } };
type fnReturn = ReturnType<typeof fn>;   // { x: number, y: number }

注意要配合 typeof fn 使用:ReturnType 接受的是函数类型typeof fn 才拿到 fn 的类型。

小结:一张关系图

工具类型作用处理对象
Pick<T, K>挑出指定字段对象接口
Omit<T, K>排除指定字段对象接口
Partial<T>所有属性变可选对象接口
Exclude<T, U>剔除联合类型成员联合类型
Record<K, V>定义键值映射字典结构
ReturnType<T>推导函数返回值函数类型
keyof T取所有键的联合类型基础操作

核心记法Omit<T, K> = Pick<T, Exclude<keyof T, K>>。把这一个等价关系吃透,PickOmitExcludekeyof 之间的关系就都通了。

使用心法:优先用工具类型去「派生」新类型,而不是重复手写接口。这样源类型一改,所有派生类型自动跟着变,既省事又不容易漏。