从一个真实场景说起
想象你在一家大公司做后台管理系统,数据库里有一张用户表:
ts
interface User {
id: number;
name: string;
age: number;
email: string;
}
问题来了:不同的页面需要的字段不一样。列表页只需要 id 和 name;编辑页只需要 name 和 age;完整信息页才需要全部字段。难道要为每个页面重新定义一套接口吗?
ts
interface UserListItem { id: number; name: string; }
interface UserEditItem { name: string; age: number; }
interface UserFull { id: number; name: string; age: number; email: string; }
代码越写越多,一旦用户表加了一个字段,所有相关接口都要跟着改,这就是典型的"类型爆炸"。TypeScript 早就想好了对策:它提供了一组工具类型(Tool Types),让你可以在已有类型的基础上加工出新类型,就像用同一套积木拼出不同的造型。
Pick:从类型里"挑选"字段
ts
type UserPreview = Pick<User, 'id' | 'name'>;
Pick 的语法是 Pick<来源类型, '要保留的字段联合'>。这里的 'id' | 'name' 叫联合类型(Union Type) ,意思是"id 或者 name"。Pick 会把 User 中这两个字段挑出来,拼成一个全新类型,列表页直接就能用:
ts
const u: UserPreview = { id: 1, name: '祖豪' }; // 不需要 email,类型层面就保证了
Omit:反过来,把不要的字段"删掉"
如果字段很多,只想去掉一两个,用 Pick 就得把所有保留字段写一遍,太啰嗦。这时轮到 Omit:
ts
type UserSafe = Omit<User, 'email'>;
意思很直白:从 User 里去掉 email 字段,剩下的全保留 。email 属于敏感信息,用 Omit 排除后,从类型层面就杜绝了"不小心把 email 发给别人"的可能。
keyof:把对象变成"钥匙串"
要理解工具类型内部是怎么工作的,先认识 keyof。它能取出对象类型的所有键,并拼成联合类型:
ts
type UserKeys = keyof User; // 'id' | 'name' | 'age' | 'email'
看,User 里的每个字段名都变成了一把"钥匙",串在了一起。
Exclude:从联合类型里"剔除"
Exclude 处理的对象不是对象类型,而是联合类型:
ts
type AfterExclude = Exclude<'id' | 'name' | 'age' | 'email', 'email'>;
// 结果: 'id' | 'name' | 'age'
语义非常清楚:把第二个参数从第一个参数中剔除,剩下的原样返回。注意它俩的分工:Exclude 操作联合类型,Omit 操作对象类型,这是最本质的区别。
手写一个 Omit:揭开工具类型的"底牌"
有了 keyof、Exclude、Pick 三件套,我们就能自己实现 Omit 了------这正是 2.ts 演示的思路:
ts
type MyOmitUser = Pick<User, Exclude<keyof User, 'email'>>;
一步步拆解:
keyof User→ 拿到所有键:'id' | 'name' | 'age' | 'email'Exclude<..., 'email'>→ 把 email 剔除,剩下'id' | 'name' | 'age'Pick<User, 剩下的键>→ 从 User 里挑出剩余字段,得到没有 email 的新类型
最终结果和 Omit<User, 'email'> 完全一致。**工具类型不是魔法,它们就是普通类型操作符的组合。**这正是 TypeScript 最强大的地方------类型本身也是一种"数据",可以被计算、被组合。打开官方源码你会发现,Omit 的实现就是这个思路。
Partial:让所有字段都可选
接口更新(Patch)是开发中的高频操作,但用户可能只改一个字段,总不能每次请求都带上全部字段:
ts
type PartialUser = Partial<User>; // 所有字段都变成可选
const patchUser: PartialUser = { name: '明明', age: 18 }; // 合法
const emptyObj: PartialUser = {}; // 也合法
Partial 会给每个字段都加上问号。要注意:可选字段的底层类型其实是 T | undefined,使用前一定要做空值判断,否则容易踩坑。
Record:快速生成键值映射
后台里"状态码 → 错误提示"是最经典的应用场景:
ts
type ErrorMsgMap = Record<number, string>;
const errorMessage: ErrorMsgMap = {
400: '请求参数错误',
404: '资源找不到',
500: '服务器内部错误',
};
function getErrMsg(code: number): string {
return errorMessage[code] ?? '未知错误';
}
Record<键类型, 值类型> 直接声明"键是 number、值是 string"的对象。再配合 ??(空值合并运算符),取值时就能优雅地兜底,不用写一堆 if-else。
ReturnType:白嫖函数的返回类型
函数的返回类型明明写在函数体里,但如果你想在别处复用这个类型,手动抄一遍既麻烦,又容易和实现脱节。ReturnType 帮你"偷懒":
ts
function fn() { return { x: 1, y: 2 }; }
type fnReturn = ReturnType<typeof fn>; // { x: number; y: number }
注意参数是 typeof fn,因为类型层面需要一个"值"才能推导出它的返回类型。