TypeScript这些工具类型,够你应对90%的业务场景了
告别手写类型,8个工具组合,轻松应对90%业务场景。
咳咳,大伙看见TypeScript也别急着划走,我知道这玩意儿在简历上写的是“熟悉”,但真到业务里手写类型的时候,估计不少小伙伴都是一顿any走天下,或者疯狂ctrl+c、ctrl+v现成的类型。今天咱们来聊聊TypeScript里的工具类型(Utility Types),这玩意儿简直就是类型世界的瑞士军刀,掌握好了,能让你的代码从“能用”直接升级到“优雅”。
要知道以前我们写 TypeScript,光是定义类型就占了一大半时间,一个接口写下来,字段多的能绕地球三圈。但其实 TypeScript 早就给我们准备好了很多现成的工具类型,用起来那叫一个酸爽。本文咱们就挑几个最常用的来好好唠唠,保证你看完之后,见人说人话,见类型写类型。
什么是工具类型?
经常对接接口的小伙伴都知道,后端接口返回的数据结构一变,我们就得跟着改 TypeScript 类型定义。改动一个字段,后面一串地方都要跟着改,一个不小心漏了,线上就开始报红。
我自己就踩过这个坑。有一次后端把userName改成了nickname,我自认为全局替换得很彻底,结果上线后一个隐藏很深的日志上报组件直接炸了——控制台飘红的那一刻,产品经理就站在我身后。那一刻我深刻体会到:手写类型就是在给自己埋雷。
工具类型存在的意义就是:让你用组合的方式构建类型,而不是每次都从头手写。就像乐高积木,你不需要每次都从塑料粒子开始,直接拼就行了。举个例子,假设你有个用户信息接口:
1 | |
现在产品经理说,用户资料页面只需要展示这些字段,但都可以不填(都是可选的)。正常写法是:
1 | |
这有两个问题:第一,重复代码,看着就烦;第二,如果哪天 User 加了一个字段 phone,UserForm 不会自动同步,还得手动加。用 Partial 工具类型,一行搞定:
1 | |
以后 User 加字段,UserForm 自动跟上,舒服。
Record——键值映射的利器
先来聊聊 Record,这玩意儿在业务里出场率贼高,经常用来做数据字典或者配置映射。
基本用法
Record<K, V>接受两个类型参数,K作为键的类型,V作为值的类型。咱们直接看例子:
1 | |
这下明白了吧?Record<MenuKey, MenuLabel> 就相当于 { home: string; profile: string; settings: string; },但是比手写清晰多了,而且当你的MenuKey多了一个选项时,TypeScript会直接报错提醒你menuLabels还差一个没填。
Record的键不一定是联合类型,也可以是 string 或 number。比如你要存一个用户 ID 到用户信息的映射:
1 | |
这里 Record<string, UserInfo> 等价于 { [key: string]: UserInfo }。但用 Record 写起来更简洁,语义也更明确——“这是一个键值映射”。
Recordable
我们经常会在项目中看到Recordable这个类型,虽然看着和上面的Record长得差不多,包括笔者刚开始看到的时候也是一脸懵,这两者到底有什么区别?于是笔者也替大家去查了查。
其实Recordable并不是 TypeScript 官方内置的工具类型,而是各大开源项目(尤其是 Vue 3 生态和某些后台管理模板)中自定义的一个常见类型。我们在项目的global.d.ts全局声明文件中能看到它的身影,定义通常长这样:
1 | |
也就是说,Recordable
我猜有两个原因:第一,Record<string, T> 写起来有点啰嗦,每次都要敲string和逗号;第二,Recordable这个词更“语义化”——表示“可以用字符串键去记录的”对象。在动态性很强的场景(比如处理 API 返回的任意 JSON 数据)中,你往往不知道对象里具体有哪些键,只知道值大概是什么类型,这时Recordable就非常顺手。
举个例子,后端有时候会返回一个结构不确定的配置对象:
1 | |
用 Recordable 你可以快速声明“这是一个键为字符串、值可以为任意类型的对象”,而不用写冗长的索引签名;总结一下:
Record<K, V>:键和值都需要显式声明,适合结构固定的场景。Recordable<T>:自定义的便捷类型,键固定为string,值默认为 any,适合处理不确定键名的动态对象。
实际业务场景
经常写后台管理系统的同学肯定知道,这种配置写法简直不要太常见:
1 | |
而且这里还有个好处,如果你漏写了一个状态码,TypeScript会在编译阶段就给你标红,再也不用担心线上少了个判断导致用户体验崩了。

Partial和Required——字段的开关
接下来这两个工具类型,简直就是应对“可选”和“必填”需求的最佳拍档。我第一次见到它们的时候,脑子里只有一个想法:以前手写那么多可选接口,简直是在浪费生命。
Partial:把属性都变成可选的
Partial
1 | |
可以看到,我们没有为“部分更新”专门写一个 UserUpdatePayload 接口,Partial<User>一行就搞定了。特别适合那种 PATCH 请求——传什么就更新什么,没传的字段保持不变。
不过这里有个小坑要注意:Partial只会处理一层属性。如果你的类型嵌套了对象,内部对象的属性不会被变成可选。
1 | |
要处理深层可选,你需要用到递归的 DeepPartial,不过那是进阶话题了,我们等一下再聊。
Required:把属性都变成必填的
有打开的就有关上的,Required<T> 的作用正好相反,把所有可选属性变成必填的。这个在表单提交场景特别有用:
1 | |
组合使用
有时候,你会遇到更变态的需求:某个对象的大部分字段可选,但某几个字段必须填。比如更新用户信息时,id必须传,其他字段可选。这时候可以把 Partial 和 Pick / Omit 组合起来:
1 | |
这种“半开半关”的组合,在实战中非常常见。用熟了之后,你会发现 TypeScript 的类型系统就像乐高——你能用几块基础积木拼出各种奇怪的形状。
源码解读
扒开 TypeScript 的源码(其实是看内置类型定义),Partial 的实现非常干净:
1 | |
就三行代码。核心就是映射类型 [P in keyof T] 遍历 T 的所有属性,然后在后面加一个?,把它变成可选的。没了。这大概就是“少即是多”的典范。
Required 的实现也类似,不过它是把可选标记移除:
1 | |
注意那个 -?——这个减号用于移除可选修饰符。TypeScript 在映射类型中允许你用 + 或 - 来添加或移除 readonly 和 ? 修饰符。-? 的意思是“把这个属性的可选标记给我去掉”,于是所有属性都变成必填了。
如果你好奇,+? 也可以显式添加可选标记,不过因为 ? 本身默认就是添加,所以一般直接写 ? 就够了。但 -? 这种写法,第一次见的时候可能会愣一下——我刚看到的时候还以为是什么正则表达式。
Pick和Omit——挑挑拣拣选属性
这两个工具类型,用一句话概括就是:Pick 是挑你要的,Omit 是排除你不要的。就像你在食堂打菜——Pick 是“我要番茄炒蛋和红烧肉”,Omit 是“除了香菜和姜,其他都给我来点”。
Pick:选出一部分属性
Pick<T, K>从类型 T 中挑出一组属性 K,生成一个新的类型。非常适合用来“裁剪”大对象。
1 | |
看到这里你可能会问:要是 User 里没有 password 怎么办?别急,Pick 只能挑已经存在的属性。如果你需要额外加字段,可以跟 & 合并:
1 | |
这个组合在实际项目中非常常见——接口返回的 User 可能有敏感字段,你挑出需要的,再补上额外参数。
Omit:排除某些属性
Omit<T, K>从类型 T 中排除一组属性 K,剩下的全部保留。适合“去掉敏感字段”或“去掉冗余字段”。
1 | |
看到区别了吗?当你要去掉的字段很少,保留的字段很多时,Omit 比 Pick 写起来更省事。反之,如果要保留的字段很少,就用 Pick。
怎么选?
至于什么时候用 Pick 还是 Omit,这个主要看业务逻辑。如果你排除的字段少,那就用 Omit;如果你要的字段少,那就用 Pick。没有固定标准,灵活选就行。
1 | |
真实业务场景
真实世界的需求总是变态的。比如:更新用户信息时,id 必须传,其他字段可选,但createdAt不能传(只读)。
1 | |
拆解一下:
- Omit<User, ‘id’ | ‘createdAt’> → 得到 { name: string; email: string }
- Partial<…> → { name?: string; email?: string }
- 再交叉 { id: number } → 最终类型
这种组合练习多了,你会发现自己越来越像在用函数式编程写类型——每个小工具都不复杂,叠在一起就能解决复杂问题。

Nullable和NonNullable——null 的过滤器
接下来这两个工具类型,专门处理一个让所有前端头疼的问题:null 和 undefined。说实话,我写 TypeScript 这两年,报错有一半都是”对象可能为 null”或者”类型 undefined 不能赋值给类型 string”。这两个工具就是专门治这个的
NonNullable:去掉null和undefined
这个工具类型从名字上就能看出来,它负责从联合类型中踢掉null和undefined。
1 | |
这玩意儿在哪个场景最有用?处理 API 响应里的可选字段。后端经常把没有值的字段返回成 null,前端处理起来烦得要死,满屏都是 if (data.field === null) return。用 NonNullable 可以帮你把类型收紧:
1 | |
不过我得说句实话——NonNullable 只在类型层面起作用。它帮你把 string | null 收窄成 string,但如果运行时真的传了 null 进来,该崩还是得崩。它就像一个”只查票不抓人”的检票员——确保你的代码逻辑上说得通,但不保证运行时数据真的干净。
场景一:filter(Boolean) 的简写形式
我们看下下面的代码:
1 | |
filter(Boolean) 是很多老手爱用的简写,但 TypeScript 并不会自动收窄类型,它只是把 Boolean 当成一个普通的函数;如果不借助NonNullable,我们就需要使用类型断言来收窄类型。
1 | |
我们使用NonNullable来主动告诉编译器:“过滤之后,空值已经没了,放心吧。”
1 | |
不过说实话,我个人更倾向于写filter(u => u !== null && u !== undefined) 而不是 filter(Boolean)——多敲几个字,换来 TypeScript 自动收窄,我觉得这笔买卖挺划算的。
源码解读
NonNullable的实现极其简洁,用到了TypeScript的条件类型:
1 | |
翻译成人话:如果 T 是 null 或 undefined,就把它替换成 never(永远不存在的类型),否则保留 T 本身。在联合类型中,never 会被自动过滤掉——所以string | null | undefined 经过这个处理后,只剩下string。
这个写法非常巧妙:它利用了条件类型的分配律——当T是联合类型时,TypeScript会对联合中的每个成员分别应用条件,最后把结果再组合成新的联合。
Nullable:把属性变成可选的且可为null
看完NonNullable,你可能想问:那反过来呢?有没有一个工具可以把所有属性都变成可空的?答案是——TypeScript 官方没有,但我们可以自己造。这就是大家常说的Nullable
1 | |
这个版本相当于给类型”增加了一个 null 的可能性”。但它只能处理单个类型,没法处理对象里的每个属性。所以更常见的是这个版本:
1 | |
经常处理数据库映射的同学,对这种类型肯定不陌生。很多 ORM 库(比如 TypeORM、Prisma)在映射数据库字段的时候,会把那些 DEFAULT NULL 的字段映射成可空类型。用 Nullable 来处理,比手写一百个 | null 省力太多了。

Exclude和Extract——联合类型的筛选器
这两个工具类型专门用于处理联合类型,用好了能帮你解决很多看似复杂的类型问题。我第一次见到它们的时候,脑子里浮现的画面是那种带筛网的淘金盘——Exclude 是把沙子筛掉留下金子,Extract 是直接捞起金子。本质上都是筛选,只是视角不同。
注意Exclude/Extract和Pick/Omit的区别,前者是筛选联合类型,后者是筛选对象属性。
Exclude:排除联合类型中的某些成员
1 | |
有次我在写一个事件总线,需要把鼠标事件和键盘事件分开处理。一开始我手写了一个新类型,把所有事件名重新列了一遍。后来同事 review 代码的时候说:”你这不是有 Event 了吗?用Exclude筛一下不就行了?”我当场愣住——确实,我列了 8 个事件名,其实只需要排除 3 个。用 Exclude 一行搞定,还少敲了 20 个字符。
Extract:选出联合类型中的某些成员
Extract正好相反,它是把符合条件的成员捞出来:
1 | |
源码解读
把 Exclude 和 Extract 的源码放在一起对比,你就能发现 TypeScript 设计者的巧思:
1 | |
看到了吗?这两行代码唯一的区别就是 never 和 T 的位置调换了。一个把符合条件的变成 never(扔掉),一个把不符合条件的变成 never(扔掉)。
要真正理解这两行代码,你得先搞明白TypeScript 的条件类型分配律。用人话解释就是:当T是一个联合类型时,TypeScript 会把条件类型分别应用到联合的每一个成员上,然后把所有结果再拼成一个新的联合。
never在这个机制里扮演的角色就是”丢弃”。条件为真时丢弃(Exclude)还是条件为假时丢弃(Extract),决定了这两个工具的行为刚好相反。
ReturnType和Parameters——函数的解构师
这两个工具类型专门用来从函数类型中提取信息,用好了能让你的代码解耦很多,再也不用在”改了一个函数的返回类型,结果十个地方都要手动改”这种事上浪费时间了。
ReturnType:获取函数返回值类型
ReturnType<T>接受一个函数类型,然后返回它的返回值类型。注意,它接受的是类型,不是函数本身——所以得用typeof先把函数转成类型。
1 | |
这个语法初次看起来有点绕:typeof fetchUser是”fetchUser 这个函数的类型”,ReturnType<…> 是”从这个类型里提取返回值”。两层套在一起才拿到最终的结果。
实际场景:API 封装
后端接口返回的数据类型,是前端最常需要复用的。用 ReturnType 可以做到定义一次,到处使用:
1 | |
Awaited 是 TypeScript 4.5 引入的工具类型,专门用来”拆”Promise。配合 ReturnType 使用,可以干净地拿到异步函数实际返回的数据类型。以前我都是先写一个 interface User { … },再写 type UserList = User[],再写函数用 Promise
说实话,用好这个组合有个小前提:你的函数必须显式声明返回类型,或者 TypeScript 能自动推断出足够具体的类型。如果函数返回的是 any 或者一个很大很宽泛的类型,那 ReturnType 也没办法给你精确的类型信息。
Parameters:获取函数参数类型
Parameters<T>返回的是一个元组类型,包含了函数所有参数的类型。对于函数重载,它会取最后一个重载的参数列表(通常是实现签名)。
1 | |
实际场景:把函数参数传给另一个函数
有一种场景特别适合Parameters:你有一个高阶函数,它接受一个函数作为参数,然后需要调用这个函数——但你不想把参数类型再重新定义一遍。
1 | |
以前写这种通用函数的时候,我常常用…args: any[]糊弄过去,把类型检查的事抛给调用方。有了Parameters,通用函数也能精准地保留参数类型,调用方传错参数时 TypeScript 会直接报红——这就把”谁该对类型负责”的问题从调用方转移到了函数定义方,更加合理。
总结
好啦,今天咱们聊了这么多工具类型,最后来总结一下。Record用于键值映射,Partial/Required处理属性的可选和必填,Pick/Omit让你挑选属性,NonNullable过滤null和undefined,Exclude/Extract处理联合类型,ReturnType/Parameters 提取函数信息。这些工具类型就像是TypeScript给我们准备好的乐高积木,掌握了这些,你就能搭出各种造型的代码。
说实话,用了这些工具类型之后,我明显感觉自己的 TypeScript 代码质量上了一个台阶——类型定义文件少了将近一半,重复代码也少了。更重要的是,改代码的时候胆子大了,因为类型系统帮我兜着底,漏改的地方TypeScript会直接报红,而不是等到上线后才炸。
说到底,工具类型就是这么一种东西——它不会让你变成一个更好的程序员,但会让你变成一个更懒、也更不容易犯错的程序员。而在这个行业里,懒和稳,往往是同义词。
快去把你项目里的any和重复接口删了吧,它们早该退休了。
参考
本网所有内容文字和图片,版权均属谢小飞所有,任何媒体、网站或个人未经本网协议授权不得转载、链接、转贴或以其他方式复制发布/发表。如需转载请关注公众号【前端壹读】后回复【转载】。