为什么需要工具类型?
类型写得好的项目,维护起来像看文档;类型写得烂的项目,改一行代码要猜半小时。
一个 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,不需要 email、age 这些敏感或大字段;详情接口返回的 DTO 也可以用 Pick 缩小范围。说白了,Pick 就是给类型做"字段精选"。
Omit:反向剔除字段
Omit 和 Pick 相反,是"不要哪些字段"。
ini
type UserSafe = Omit<User, 'email'>;
const safeUser: UserSafe = {
id: 2,
name: 'l',
age: 20
};
场景:用户信息要脱敏,不发 email 给前端;更新用户时排除 id(id 由路径参数决定)。
Omit 的内部实现,为什么等价于 Pick + Exclude?
TS 源码中 Omit 的实现是:
scala
type Omit<T, K extends keyof any> = Pick<T, Exclude<keyof T, K>>;
怎么理解?拆成三步:
keyof T拿到所有键的联合类型,例如'id' | 'name' | 'age' | 'email'Exclude<keyof T, K>把要剔除的K从联合类型中删掉,剩下需要保留的键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 获取函数类型,而不是 fn。ReturnType 在 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 是不是已经够用了。