TypeScript 工具类型七件套:从裁剪到组合,掌握类型推导的核心武器

11 阅读10分钟

TypeScript 工具类型七件套:从裁剪到组合,掌握类型推导的核心武器

写 TypeScript 到一定阶段,会频繁遇到一类需求:从一个已有的对象类型,推导出另一个相关的类型——"挑几个字段出来""去掉某个敏感字段""所有字段都变可选"。这些需求如果每次都手写一遍,既啰嗦又容易漏。TypeScript 内置了一批**工具类型(Utility Types)**专门干这个,本文就把最常用的七个一次性讲清,并深入拆解 Omit 的底层实现,让你不仅会用,还能自己拼。

在复杂的、大型的项目里,类型消费比较大,用好这些工具类型能大幅减少重复的类型定义。而其中最值得深挖的,是 Omit 那句"等价于 Pick<T, Exclude<keyof T, K>>"——它背后藏着一套可复用的类型推导思路。


一、起点:一个 User 接口

所有的工具类型都围绕同一个源类型展开。我们先定义一个最朴素的 User

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

后面的每一个类型,都是从 User 这个"原材料"里裁剪、变换出来的。理解了"以 User 为源"这个前提,七个工具类型就有了统一的参照系。


二、Pick:挑几个字段出来

第一个需求:从一个类型中挑选一些你需要的字段,形成新的类型。对应的工具类型是 Pick

// Pick 从类型中挑选一些字段,形成新的类型
type UserPreview = Pick<User, 'id' | 'name'>

const u: UserPreview = {
  id: 1,
  name: '张三',
  // age: 18,   // 报错:UserPreview 里没有 age
}

Pick<T, K> 的签名是 Pick<T, 选取类型的联合字符串>——第一个参数是源类型,第二个参数是你想保留的字段名组成的联合类型。Pick<User, 'id' | 'name'> 得到的新类型只含 idname,多写一个 age 编译器就会拦下来。

典型场景:列表预览。列表页只想展示几个字段,用 Pick 裁出一个"轻量版"类型,既约束了传入的数据形状,又不用维护一份重复的字段清单。


三、Omit:排除某个字段

Pick 正好相反的需求是排除一些字段——比如把敏感字段 email 去掉再对外输出:

// Omit 从类型中排除一些字段,形成新的类型
type UserSafe = Omit<User, 'email'>

const uSafe: UserSafe = {
  id: 2,
  name: '廖昊',
  age: 19,
  // email 已经不存在了
}

Omit<T, K> 的签名是 Omit<T, 排除类型的联合字符串>:第二个参数是你要删掉的字段。Omit<User, 'email'> 得到的 UserSafe 保留了除 email 外的全部字段。

Pick 和 Omit 是一对镜像Pick 白名单式地"留",Omit 黑名单式地"删"。当要删的字段少时用 Omit 更省事(一个 'email' 就够),当要留的字段少时用 Pick 更清晰。


四、Partial:让所有字段变可选

第三个常见需求是 "patch" 修改——更新对象时只传要改的字段,没传的保持原值:

// Partial 所有字段全部可选
type PartialUser = Partial<User>

// patch 修改,对象属性很多
const patchUser: PartialUser = {
  name: '廖昊',
  age: 19,
}

Partial<T>User 的每个字段都变成可选,所以 patchUser 可以只写 nameage,不写 idemail

落地场景:对象属性很多的时候做部分更新,配合后端 PATCH 语义(只改一部分)非常契合。比如更新用户信息,前端只需构造一个包含待改字段的对象,类型上使用 Partial<User> 就天然允许缺省。


五、Record:构造一个键值字典

Record 和前面几个不同,它不是"裁剪",而是构造——根据键类型和值类型,造出一个字典:

// JSON key:value  Record<键类型, 值类型>
type Dict = Record<string, number>
const obj: Dict = { a: 1, b: 2 }

Record<K, V> 的两个参数分别是键的类型和值的类型。Record<string, number> 就是"字符串键、数字值"的字典。

一个特别实用的例子——HTTP 状态码 → 错误信息的映射:

type ErrorMsgMap = Record<number, string>

const errorMsgMap: ErrorMsgMap = {
  400: '请求参数错误',
  401: '未登录,需要先登录',
  403: '权限不足,禁止访问',
  404: '资源不存在',
  500: '服务器内部错误',
}

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

这里 Record<number, string> 把"状态码(number)→ 提示文案(string)"这个映射关系固化成类型,再配合 ?? 兜底一个"未知错误"。Record 让"键值查找表"有了类型保障,getErrorMsg 里查不存在的 code 也不会返回 undefined


六、ReturnTypeExclude:两种"取出来"

还有两个工具类型,分别从"函数"和"联合类型"里取东西。

6.1 ReturnType:取函数的返回值类型

function fn() {
  return { x: 1, y: 2 }
}
// ReturnType<函数类型> 函数返回值类型
type FnReturn = ReturnType<typeof fn>  // { x: number, y: number }

注意写法是 ReturnType<typeof fn>——ReturnType 要的是函数类型,而 fn函数值,所以要先用 typeof fn 拿到它的类型再传入。好处是:如果 fn 的返回值结构变了,FnReturn 会自动跟着变,不用手动同步。

6.2 Exclude:从联合类型里删除指定成员

type All = "id" | "name" | "age" | "email"
// Exclude<联合类型, 排除的类型> 排除联合类型中的指定类型
type AfterExclude = Exclude<All, "email">  // "id" | "name" | "age"

Exclude<联合类型, 排除的类型> 从联合类型里删掉指定成员。

一个重要辨析Omit 处理对象接口,Exclude 处理联合类型。即:Omit 操作的是对象类型(去掉对象的字段),Exclude 操作的是联合类型(去掉联合里的成员)。两者的对象不一样,别混用。


七、核心:Omit 的三步底层实现

笔记最精彩的地方,是把 Omit 这个"黑盒"拆开了。开头那句:

Omit<T, K> 等价于 Pick<T, Exclude<keyof T, K>>,怎么理解?

答案藏在三个中间步骤里:

// ① keyof 获取对象的所有属性名
type UserKeys = keyof User                      // "id" | "name" | "age" | "email"

// ② 从联合类型里排除 'email'
type KeepKeys = Exclude<UserKeys, 'email'>      // "id" | "name" | "age"

// ③ 从原类型中 Pick 出需要保留的键
type MyOmitUser = Pick<User, KeepKeys>          // { id: number, name: string, age: number }

拆解成三步,Omit 的底层逻辑就一目了然:

  1. keyof T 拿到所有键的联合类型——keyof User 得到 "id" | "name" | "age" | "email"
  2. Exclude 把要剔除的 K 删掉,剩下需要保留的键——Exclude<UserKeys, 'email'> 得到 "id" | "name" | "age"
  3. 再用 Pick 把剩下的键从 T 中提取出来——Pick<User, KeepKeys> 得到 { id, name, age }

三步走完,就得到了 Omit<T, K>。这就是 TypeScript 内部 Omit 类型的实现原理:Omit 本身不是原子操作,而是 PickExcludekeyof 三个工具类型组合出来的。

这个拆解的价值不只是"看懂 Omit",更在于它演示了一种思考方式——用几个简单工具类型拼出更复杂的类型。遇到没有现成工具类型能满足的需求时,学着像这样"拆成 keyof → Exclude → Pick 的组合"自己拼一个,是进阶 TypeScript 的核心能力。


八、七件套全景

最后把七个工具类型收进一张表,方便对照记忆:

工具类型签名作用操作对象
PickPick<T, K>挑选字段,形成新类型对象类型
OmitOmit<T, K>排除字段,形成新类型对象类型
PartialPartial<T>所有字段变可选对象类型
RecordRecord<K, V>构造键值字典键 + 值
ReturnTypeReturnType<typeof fn>取函数返回值类型函数类型
ExcludeExclude<U, K>从联合类型里排除成员联合类型
keyofkeyof T取对象所有键的联合类型对象类型

记忆要点两条Omit 操作对象接口、Exclude 操作联合类型;Omit = Pick + Exclude + keyof 的组合。抓住了这两条,七件套的骨架就立住了。


九、进阶组合:自己拼出更多工具类型

理解了 Omit 的组合原理,你可以自己拼出更多的工具类型。例如:

// 让指定字段变为必选(与 Partial 相反)
type Required<T> = { [P in keyof T]-?: T[P] }

// 提取对象中值为特定类型的字段
type PickByType<T, U> = { [P in keyof T as T[P] extends U ? P : never]: T[P] }

// 从类型中排除值为特定类型的字段
type OmitByType<T, U> = { [P in keyof T as T[P] extends U ? never : P]: T[P] }

这些都可以基于 keyof、映射类型、条件类型组合出来。TypeScript 的类型系统是图灵完备的,组合思维比记住每一个工具类型更重要。


十、面试高频问题与答题框架

Q1:PickOmit 有什么区别?分别用在什么场景?

回答框架

Pick<T, K> 从类型里挑选一些字段形成新类型(白名单式"留"),Omit<T, K> 从类型里排除一些字段形成新类型(黑名单式"删")。要删的字段少(如去掉 email)用 Omit 更省事,要留的字段少(如列表只展示 id/name)用 Pick 更清晰。二者是一对镜像。

Q2:Omit<T, K> 等价于 Pick<T, Exclude<keyof T, K>>,怎么理解?

回答框架

拆成三步:① keyof T 拿到所有键的联合类型;② Exclude 把要剔除的 K 删掉,剩下需要保留的键;③ 再用 Pick 把剩下的键从 T 中提取出来,最后得到 Omit<T, K>。所以 Omit 不是原子操作,而是 Pick + Exclude + keyof 三个工具类型组合出来的,这正是 TypeScript 内部 Omit 的实现原理。

Q3:Partial 的典型使用场景是什么?

回答框架

部分更新(patch)。更新对象时只传要改的字段、没传的保持原值,比如 const patchUser: Partial<User> = { name: '廖昊', age: 19 } 可以只写两个字段。Partial<T> 把每个字段都变可选,对象属性很多时特别有用,与后端 PATCH 的"只改一部分"语义契合。

Q4:Record 是什么?ExcludeOmit 的对象有什么不同?

回答框架

Record<K, V> 根据键类型和值类型构造一个键值字典,如 Record<number, string> 表示"数字键、字符串值",常用于 HTTP 状态码 → 错误信息的映射表。辨析上:Omit 操作的是对象接口(去掉对象的字段),Exclude 操作的是联合类型(去掉联合里的成员),两者对象不同,别混用。

Q5:ReturnType 为什么要写 ReturnType<typeof fn> 而不是 ReturnType<fn>

回答框架

ReturnType 要的是函数类型,而 fn函数值,所以要先用 typeof fn 拿到它的类型再传入。这样如果 fn 的返回值结构变了,ReturnType 推导出的类型会自动跟着变,不用手动同步。

Q6:能否自己组合出新的工具类型?举个例子。

回答框架

可以。TypeScript 的类型系统支持映射类型和条件类型,我们可以基于这些基础能力组合出新工具。比如提取对象中值为特定类型的字段:

type PickByType<T, U> = { [P in keyof T as T[P] extends U ? P : never]: T[P] }

这种组合思维比记住每一个现成工具类型更重要。


结语:工具类型是"类型推导的组合拳"

本文没有引入新的语言特性,却把一个进阶 TypeScript 开发者绕不开的技能点打透了——用内置工具类型,从已有类型推导出新类型。回看整条线,其实是两层认知:

  • 会用Pick/Omit/Partial/Record/ReturnType/Exclude/keyof 各自解决一类裁剪/构造需求。
  • 会拆Omit = Pick + Exclude + keyof——用简单工具类型组合出复杂类型。

落到记忆上就两条:Omit 管对象、Exclude 管联合;OmitPick + Exclude + keyof 的三步组合。

动手前,拿这份清单自检:

  • 能否说清 Pick<T, K>(挑选)和 Omit<T, K>(排除)是一对镜像?
  • 能否把 Omit = Pick<T, Exclude<keyof T, K>> 拆成 keyof → Exclude → Pick 三步讲清楚?
  • 能否说出 Partial<T> 用于 patch 部分更新的场景?
  • 能否用 Record<number, string> 构造一个状态码 → 错误信息的映射表?
  • 能否解释 ReturnType<typeof fn> 里为什么要加 typeof
  • 能否分清 Omit 操作对象接口、Exclude 操作联合类型?
  • 能否说出 keyof T 拿到的是"所有键的联合类型"?

掌握这七个工具类型,尤其是 Omit 背后的组合思路,就拿到了类型推导的第一套组合拳——裁剪用 Pick/Omit、可选用 Partial、字典用 Record、取值用 ReturnType/Exclude/keyof,复杂需求自己拼。这不仅是记住几个 API,更是理解 TypeScript 类型系统组合之美的第一步。