🚀 别再只会 any 了!3 个真实业务场景带你搞懂 TS 工具类型底层原理

0 阅读4分钟

项目里明明用了 TypeScript,结果写着写着一堆 anyunknown,类型失效还以为自己写得很安全。其实,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 把 onClickButtonProps 中移除,当别的开发使用该组件时就不能直接传递 onClick 事件了。

我们看下 Omit 底层实现:

type Omit<T, K extends keyof any> = Pick<T, Exclude<keyof T, K>>
  • keyof T 取出所有属性名
  • Exclude<keyof T, K> 去掉不要的
  • Pick 剩下的属性

Omit 其实是通过组合 PickExclude 这两个工具类型完成的。

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,啥属性不想让人传,直接干掉,省得后面踩坑。顺带还学习了 ExcludePick,组合拳了解一下。

接口返回值可以偷懒了!ReturnType + Awaited,让 TypeScript 帮你推出来,再也不用怕手写类型漏改了。

工具类型并非可有可无,用好这些工具能提升我们的代码健壮性,让我们的代码少出问题少返工。

如果你觉得这篇文章对你有帮助,欢迎点赞 👍、收藏 ⭐、评论 💬 让我知道你在看~ 我会持续更新 前端打怪笔记系列文章,👉 记得关注我,不错过每一篇干货更新!❤️