TS 工具类型实战:Pick/Omit/Partial/Record 一次讲透,别再 Omit 反了

为什么需要工具类型?

类型写得好的项目,维护起来像看文档;类型写得烂的项目,改一行代码要猜半小时。

一个 User 接口可能被用在列表、详情、表单、更新、权限等多个场景,每个场景需要的字段都不一样。如果每个场景都手写 interface,不仅重复,还容易漏改。TypeScript 内置的工具类型就是帮你"消费"已有类型,快速生成新类型。

keyof:一切工具类型的地基

keyof T 会取出 T 所有键组成联合类型。

ini 复制代码
interface User {
  id: number;
  name: string;
  age: number;
  email: string;
}

type UserKeys = keyof User; // 'id' | 'name' | 'age' | 'email'

你会发现,keyof 是后面所有操作的"原料":没有它,你都不知道类型里有哪些键。

Pick:按需取字段

Pick 的字面意思就是"挑",从一个大类型里挑出你需要的字段,形成新类型。

ini 复制代码
type UserPreview = Pick<User, 'id' | 'name'>;

const u: UserPreview = {
  id: 1,
  name: 'lg'
};

场景:列表项通常只需要 id + name,不需要 emailage 这些敏感或大字段;详情接口返回的 DTO 也可以用 Pick 缩小范围。说白了,Pick 就是给类型做"字段精选"。

Omit:反向剔除字段

Omit 和 Pick 相反,是"不要哪些字段"。

ini 复制代码
type UserSafe = Omit<User, 'email'>;

const safeUser: UserSafe = {
  id: 2,
  name: 'l',
  age: 20
};

场景:用户信息要脱敏,不发 email 给前端;更新用户时排除 idid 由路径参数决定)。

Omit 的内部实现,为什么等价于 Pick + Exclude?

TS 源码中 Omit 的实现是:

scala 复制代码
type Omit<T, K extends keyof any> = Pick<T, Exclude<keyof T, K>>;

怎么理解?拆成三步:

  1. keyof T 拿到所有键的联合类型,例如 'id' | 'name' | 'age' | 'email'
  2. Exclude<keyof T, K> 把要剔除的 K 从联合类型中删掉,剩下需要保留的键
  3. Pick<T, 剩下的键> 再把这些键从 T 中挑出来

所以 Omit<User, 'email'> 等价于 Pick<User, Exclude<keyof User, 'email'>>,也就是 Pick<User, 'id' | 'name' | 'age'>

一句话:Omit 不是新魔法,它只是 Pick + Exclude 的组合拳。

Exclude 和 Omit 的区别

  • Exclude<联合类型, 要排除的成员>:从联合类型 中删除某些成员,返回新的联合类型
  • Omit<对象类型, 要排除的键>:从对象类型 中删除某些键,返回新的对象类型

Exclude 处理的是联合类型 ,Omit 处理的是对象接口

ini 复制代码
type All = 'id' | 'name' | 'age' | 'email';
type AfterExclude = Exclude<All, 'email'>; // 'id' | 'name' | 'age'

Exclude 的第二个参数可以是单个字符串,也可以是联合类型,表示要删除哪些。

一个极易踩的坑:Omit 第二个参数是"要删的键",不是"要保留的键"

看这段代码:

ini 复制代码
interface User {
  id: number;
  name: string;
  age: number;
  email: string;
}

type UserKeys = keyof User; // 'id' | 'name' | 'age' | 'email'
type KeepKeys = Exclude<UserKeys, 'email'>; // 'id' | 'name' | 'age'

type MyOmitUser = Omit<User, KeepKeys>;

如果你以为 KeepKeys 是"保留的键",那就反了。Omit<User, KeepKeys> 会剔除 id/name/age,最后只剩下 email

ini 复制代码
type MyOmitUser = { email: string };

如果你想排除 email,应该直接写:

ini 复制代码
type UserSafe = Omit<User, 'email'>; // { id: number; name: string; age: number }

记住:Omit 是"不要",不是"只要"。要保留用 Pick,要删除用 Omit。

Partial:所有字段可选

Partial<T>T 的所有字段变成可选。

ini 复制代码
type PartialUser = Partial<User>;

const patchUser: PartialUser = {
  name: 'm',
  age: 18
};

const emptyObj: PartialUser = {}; // 合法

场景:更新接口的 patch body、表单草稿、部分配置合并。对象属性很多时,Partial 能省去大量重复的 ?:

内部实现也很简单:

ini 复制代码
type Partial<T> = {
  [P in keyof T]?: T[P];
};

Record:快速构造键值对

Record<K, V> 用来构造"键类型为 K、值类型为 V"的对象。

ini 复制代码
type Dict = Record<string, number>;
const obj: Dict = { a: 1, b: 2 };

最经典的场景是 HTTP 状态码映射:

typescript 复制代码
type ErrorMsgMap = Record<number, string>;

const errorMessage: ErrorMsgMap = {
  400: '请求参数错误',
  401: '未登录,请重新登录,没有权限',
  403: '权限不足,禁止访问',
  404: '资源不存在',
  500: '服务器内部错误'
};

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

Record<number, string> 表示一个 key 是 number、value 是 string 的对象。相比 { [code: number]: string },Record 的语义更清晰,也更适合作为工具类型的输出。

ReturnType:获取函数返回类型

有时候你不想重复声明函数返回值的类型,或者第三方库没有导出返回类型,可以用 ReturnType 自动推导。

csharp 复制代码
function fn() {
  return { x: 1, y: 2 };
}

type FnReturn = ReturnType<typeof fn>; // { x: number; y: number }

注意要用 typeof fn 获取函数类型,而不是 fnReturnType 在 Redux action、Express handler、工具函数封装里很常见。

一张表总结

工具类型 作用 典型场景
keyof T 取所有键联合 动态属性、遍历
Pick<T, K> 挑选字段 列表项、DTO
Omit<T, K> 剔除字段 脱敏、排除 id
Exclude<U, K> 从联合类型中删除 联合类型过滤
Partial<T> 所有字段可选 更新、表单草稿
Record<K, V> 构造键值对 状态码、字典
ReturnType<F> 获取函数返回类型 第三方库、封装

写在最后

工具类型不是炫技,而是让类型像积木一样复用。你写得越多,越会发现:类型定义的成本不在"写",而在"改";工具类型能帮你把改动收敛到一处。

下次在项目里遇到"这个类型跟那个类型就差几个字段"的时候,先别急着新建 interface,想想 Pick、Omit、Partial 是不是已经够用了。

相关推荐
苏灿烤鱼1 小时前
十个 CLI 坐进一间办公室,协调层靠得住吗?
typescript·github·agent
BreezeJiang1 小时前
别再死记 TypeScript 工具类型:看懂 Omit 的类型数据流
typescript
breeze jiang2 小时前
TypeScript 工具类型怎么记:从 Pick、Omit 到 keyof 与 Exclude 推导
linux·ubuntu·typescript
触底反弹2 小时前
🔥 NestJS 从零到实战:一个 Todos CRUD 搞懂企业级后端框架的核心设计
后端·typescript·nestjs
用户9385156350714 小时前
TypeScript 高级类型 + CSS 三列布局:从类型体操到样式工程的进阶之路
css·面试·typescript
moMo18 小时前
深入 NestJS:从工厂模式到装饰器模式的架构之美
typescript·nestjs
烬羽1 天前
NestJS 依赖注入:Controller 里那个没有 new 的 service,到底从哪来的?
设计模式·typescript·nestjs
渣波1 天前
NestJS 企业级后端架构实战:从核心代码到工程化思维的深度重构
前端·typescript·nestjs
嘟嘟07171 天前
NestJS 入门:从 NestFactory 入口到 Module/Controller/Service 模块化结构一次讲清
typescript·node.js·nestjs