项目里明明用了 TypeScript,结果写着写着一堆
any
、unknown
,类型失效还以为自己写得很安全。其实,TS 的工具类型就是专门用来帮你写出更安全、更优雅代码的! 但很多人学工具类型只会背 API,一上项目就忘了。今天这篇文章不啰嗦 API,而是结合我自己的项目经验,用 3 个真实业务场景带你搞懂 Partial / Omit / ReturnType 等工具类型的用法和底层原理,学完能立刻用到项目里。
💡 为什么要掌握 TS 工具类型?
我在刚写 TS 的时候,总觉得工具类型只是锦上添花,不会用也没啥影响。随着后面项目越写越大,组件越来越多,才发现:
工具类型真不是可有可无,它是让你写 可维护、易扩展、高容错 代码的关键。
比如以下场景:
- 表单局部更新,少传字段直接报红,安全性高
- 封装组件,精准控制 props,防止误传
- 接口返回值变化时不用担心手写类型漏改
其实这些都能靠 TS 的内置工具类型轻松搞定,还让代码看起来更高级、更稳健😉。
🎯 常见场景下如何选择
下面我们列举几个常见的场景,看下如何用 TS 内置工具快速解决这些问题。
场景 1:表单数据局部更新
这种情况在管理后台会经常遇到,比如使用编辑用户功能,用户数据有十几个字段,但接口只需要提交被改动的字段。
可能很多人都第一反应就是:
const updateUser = (data: any) => {
// 调接口
}
🤭any 大法解决一切问题!
那这样肯定是不行的,字段写错了也没提示,漏了字段也没法检测出来,安全性很差。
正确写法:
interface User {
id: string
name: string
email: string
age: number
}
const updateUser = (data: Partial<User>) => {
// 只传改动的字段
}
// 用法
updateUser({ name: '张三' })
updateUser({ email: 'test@test.com', age: 30 })
使用 Partial 给字段设置为可选,就可以自由的传参了,错传参数也会有对应报错。
Partial 源码其实很简单:
type Partial<T> = {
[P in keyof T]?: T[P]
}
拿到 T 的每个 key,然后加上可选符 ?
,让它变成可选。
不过 Partial 不支持嵌套对象,比如:
interface User {
id: string
name: string
address: {
city: string
zip: string
}
}
Partial<User>
只会让 address 可选,不会让 address.city
可选。
但我们已经了解了 Partial 的实现,那么我们可以自己手撸一个递归版本:
type DeepPartial<T> = {
[P in keyof T]?: T[P] extends object ? DeepPartial<T[P]> : T[P]
}
// 用法
const data: DeepPartial<User> = {
address: { city: '北京' }
}
这样就能完美支持嵌套了。
场景 2:组件 props 透传时去掉某些属性
比如我们在写封装组件时,想基于第三方库的 props 复用,同时又想自定义 onClick
事件,不希望组件的使用者直接传递 onClick
事件。
我们可以使用 Omit:
import { ButtonProps } from 'antd'
type MyButtonProps = Omit<ButtonProps, 'onClick'>
export default function MyButton(props: MyButtonProps) {
const handleClick = () => console.log('自定义逻辑')
return <button {...props} onClick={handleClick} />
}
使用 Omit 把 onClick
从 ButtonProps
中移除,当别的开发使用该组件时就不能直接传递 onClick
事件了。
我们看下 Omit 底层实现:
type Omit<T, K extends keyof any> = Pick<T, Exclude<keyof T, K>>
keyof T
取出所有属性名Exclude<keyof T, K>
去掉不要的- 再
Pick
剩下的属性
Omit 其实是通过组合 Pick
和 Exclude
这两个工具类型完成的。
Exclude
是 TypeScript 的另一个工具类型,作用是从联合类型中排除某些成员:
type User = { id: number; name: string; age: number };
type RemainingKeys = Exclude<keyof User, 'age'>; // 'id' | 'name'
Pick<T, K>
用于从 T
中选取指定属性 K
,创建新类型:
type User = { id: number; name: string; age: number };
type RemainingKeys = Exclude<keyof User, 'age'>; // 'id' | 'name'
type UserWithoutAge = Pick<User, RemainingKeys>; // { id: number; name: string }
一个场景下学会 3 种内置类型 😄
场景 3:根据函数推导返回值
写请求接口的封装时,我们会写:
const fetchUser = () => axios.get<{ id: string; name: string }>('/user')
如果要单独声明返回值类型,很多人会重复写:
type FetchUserResult = Promise<{ id: string; name: string }>
一旦接口改了,你手写的类型很容易忘记同步更新。
用 ReturnType 自动推导:
type FetchUserResult = ReturnType<typeof fetchUser>
也可以结合 Awaited 拿到最终数据结构:
type Data = Awaited<ReturnType<typeof fetchUser>>
直接得到 axios.get
的响应数据类型。
ReturnType 底层长这样:
type ReturnType<T extends (...args: any) => any> =
T extends (...args: any) => infer R ? R : any
- 用条件类型匹配函数签名
- 用
infer R
推断返回值类型
🌈 小总结
看完这 3 个场景,是不是发现工具类型真的挺香?咱们简单捋一下今天说的重点:
表单局部更新? 用 Partial
或自己写个递归版 DeepPartial
,字段随便传、放心传。
组件 props 想屏蔽? 用 Omit
,啥属性不想让人传,直接干掉,省得后面踩坑。顺带还学习了 Exclude
和 Pick
,组合拳了解一下。
接口返回值可以偷懒了! 用 ReturnType
+ Awaited
,让 TypeScript 帮你推出来,再也不用怕手写类型漏改了。
工具类型并非可有可无,用好这些工具能提升我们的代码健壮性,让我们的代码少出问题少返工。
如果你觉得这篇文章对你有帮助,欢迎点赞 👍、收藏 ⭐、评论 💬 让我知道你在看~ 我会持续更新 前端打怪笔记系列文章,👉 记得关注我,不错过每一篇干货更新!❤️