当我第一次看到 TypeScript 的infer和条件类型时,反手就关了浏览器——这堆尖括号和问号到底想搞死谁?像T extends (infer U)[] ? U : never这种类型,我盯着看了 5 分钟,感觉自己像个没学过编程的傻子。
直到后来我发现:这些玩意儿根本不是黑魔法,只是穿了件吓人外套的纸老虎。只要掌握 3 条简单规则,你会和我一样拍大腿:“原来这么简单!”
先跟你说句掏心窝子的话:你不是一个人在战斗
我至今记得当初的崩溃:
- 看到
ReturnType<T>的定义T extends (...args: any[]) => infer R ? R : never,我以为这是外星语言; - 写
Exclude<T, U>时,联合类型突然“自动拆分”,我差点把键盘摔了; - 用
infer提取数组元素类型,我盯着T extends (infer U)[] ? U : never,连喝三瓶可乐才冷静下来。
但现在我想告诉你:这些恐惧都是纸老虎。TypeScript 的高级类型,本质是 3 条“说破了就没人信”的简单逻辑。
🔍 规则一:条件类型 = 给类型用的「if-else」
你写 JavaScript 时会用if (a === 1) { ... } else { ... }——条件类型就是 TypeScript 给类型写的“if-else”。
它的语法长这样:
type 类型名<T> = T extends 条件 ? 满足条件的类型 : 不满足的类型翻译成人话就是:
“嘿 TypeScript,如果这个类型符合条件,就给我返回 A 类型;不然就返回 B 类型。”
🌰 实战升级:Vue3 加载按钮的完整 Props 设计
在实际项目中,“加载状态的按钮”绝不是简单的disabled——还要处理样式类、默认文案、点击节流。我们用条件类型设计一个更真实的ButtonProps:
// src/types/button.ts
import type { PropType } from 'vue'
/**
* 按钮Props类型:根据loading状态动态调整
* @param T 是否加载中(默认false)
*/
type ButtonProps<T extends boolean = false> = {
/** 按钮文字(必传) */
label: string
/** 加载状态(控制禁用&文案) */
loading?: T
/** 按钮尺寸(默认'medium') */
size?: 'small' | 'medium' | 'large'
} & (T extends true
? {
/** 加载时的文案(默认'加载中...') */
loadingText?: string
/** 加载时的样式类(固定为'btn-loading') */
class?: 'btn-loading'
/** 加载时禁用点击(必为never,防止误传) */
onClick?: never
}
: {
/** 点击事件(非加载时必传) */
onClick: () => void
/** 正常状态的样式类(默认'btn-normal') */
class?: 'btn-normal' | 'btn-primary'
})
// Vue3组件中使用(Script Setup语法)
const props = defineProps<ButtonProps>()
const emit = defineEmits(['click'])
// 处理默认值(Vue3中defineProps的默认值需要用withDefaults)
withDefaults(defineProps<ButtonProps>(), {
size: 'medium',
loadingText: '加载中...',
class: 'btn-normal',
})📝 为什么这样设计?
- 交叉类型
&的作用:将基础 Props(label、size)与“状态专属 Props”(加载/正常)合并,保证 Props 的完整性; never的意义:加载状态下onClick设为never,直接禁止传入点击事件,避免“加载时仍能点击”的 bug;- 默认值的处理:用
withDefaults统一设置默认值,符合 Vue3 的最佳实践。
🎩 规则二:裸类型的「分发魔术」——自动遍历联合类型
这条规则我花了最久才搞懂,但一旦悟透,处理联合类型的效率直接翻倍。
🔍 先明确:什么是“裸类型”?
“裸类型”是指泛型参数没有被包裹在任何类型构造器中(如[]、{}、() =>等)。比如:
- 裸类型:
T extends any(T直接出现); - 非裸类型:
[T] extends [any](T被包裹在[]中)。
🎩 分发魔术的核心:自动拆联合、逐个判
当条件类型作用于裸类型泛型时,TypeScript 会自动:
- 把联合类型拆成单个成员;
- 对每个成员应用条件类型;
- 把结果重新拼成联合类型。
🌰 实战升级:项目中的“路由权限过滤”
在 Vue3 项目中,我们常需要从路由列表中过滤出“需要登录”的路由。用分发魔术可以轻松实现:
// src/types/route.ts
/** 路由元信息类型(控制权限) */
type RouteMeta = {
/** 是否需要登录(默认false) */
requiresAuth?: boolean
/** 路由标题(用于面包屑) */
title?: string
}
/** 路由类型 */
type Route = {
path: string
name: string
meta: RouteMeta
children?: Route[] // 支持嵌套路由
}
// 项目中的路由列表(含嵌套路由)
const routes: Route[] = [
{ path: '/', name: 'Home', meta: { title: '首页' } },
{ path: '/login', name: 'Login', meta: { title: '登录' } },
{
path: '/profile',
name: 'Profile',
meta: { requiresAuth: true, title: '个人中心' },
children: [
{
path: 'info',
name: 'ProfileInfo',
meta: { requiresAuth: true, title: '个人信息' },
},
],
},
{
path: '/admin',
name: 'Admin',
meta: { requiresAuth: true, title: '后台管理' },
},
]
// 🔥 过滤需要登录的路由(裸类型泛型实现分发)
type FilterAuthRoutes<T extends Route> = T extends {
meta: { requiresAuth: true }
}
? T
: never
// 提取所有需要登录的路由(递归处理嵌套路由)
type AuthRoutes = FilterAuthRoutes<(typeof routes)[number]>
// 结果:包含Profile(含子路由)、Admin的路由类型📝 为什么这样高效?
- 自动处理嵌套路由:即使路由有
children,分发魔术也会遍历每个子路由; - 类型安全:过滤后的
AuthRoutes仅包含需要登录的路由,避免“未登录用户访问受限页面”的 bug。
🔮 规则三:infer = 类型的「偷窥器」——提取内部信息
infer是 TypeScript 给你的“望远镜”——它能帮你从复杂类型中“偷窥”并提取内部的类型。
它的语法长这样:
type 类型名<T> = T extends 包含infer的结构 ? 提取的类型 : never翻译成人话就是:
“嘿 TypeScript,我不管这个类型里面是什么,你帮我把某个位置的类型提取出来,存到变量里,我好用它!”
🌰 实战升级:提取 Pinia Store 的 State 类型
Pinia 是 Vue3 官方推荐的状态管理库,我们常需要提取 Store 的 State 类型,用于组件中的类型提示。用infer可以轻松实现:
// src/stores/user.ts(Pinia Store)
import { defineStore } from 'pinia'
// 定义User Store
const useUserStore = defineStore('user', {
state: () => ({
id: 0,
name: '张三',
email: 'zhangsan@example.com',
isLogin: false,
}),
actions: {
login(username: string, password: string) {
// 登录逻辑
this.isLogin = true
},
},
})
// 🔥 提取State类型(用infer偷窥state函数的返回值)
type StateOfStore<T> = T extends { state: () => infer S } ? S : never
// 结果:{ id: number; name: string; email: string; isLogin: boolean }
type UserState = StateOfStore<typeof useUserStore>📝 为什么这样好用?
- 自动同步 State 变化:如果 Store 的 State 字段增加/修改,
UserState会自动更新,无需手动维护; - 类型提示更精准:在组件中使用
useUserStore()时,store.state会自动提示所有 State 字段,避免拼写错误。
🌰 再升级:提取函数的参数类型
TypeScript 内置的Parameters<T>也是用infer实现的——提取函数的参数类型,这在“封装 API 请求”时特别有用:
// 封装API请求函数
async function fetchUser(
id: number,
options?: { cache: boolean }
): Promise<UserState> {
const res = await fetch(`/api/user/${id}`, { cache: options?.cache ?? false })
return res.json()
}
// 提取fetchUser的参数类型
type FetchUserParams = Parameters<typeof fetchUser>
// 结果:[id: number, options?: { cache: boolean }]
// 使用时自动提示参数:
const params: FetchUserParams = [123, { cache: true }]
fetchUser(...params) // 正确,自动匹配参数类型🎁 彩蛋:映射类型 = 类型的「for 循环」
既然你已经掌握了前面 3 条规则,映射类型对你来说就是“小菜一碟”——它能帮你遍历类型的所有属性,批量修改。
🔍 语法回顾
type 类型名<T> = {
[K in keyof T]: 修改后的类型
}翻译成人话就是:
“嘿 TypeScript,遍历类型 T 的所有属性,每个属性都改成我要的类型。”
🌰 实战升级:给组件 Props 加“全局前缀”
在大型项目中,为了避免组件 Props 冲突,我们常给所有 Props 加一个全局前缀(比如app-)。用映射类型可以一键实现:
// 给Props加前缀的映射类型
type AddGlobalPrefix<T, Prefix extends string = 'app'> = {
// 用`as`重命名属性:将K转为`Prefix+首字母大写的K`
[K in keyof T as `${Prefix}-${Capitalize<K & string>}`]: T[K]
}
// 原始ButtonProps
type ButtonProps = {
label: string
onClick: () => void
size: 'small' | 'medium' | 'large'
}
// 加前缀后的Props
type AppButtonProps = AddGlobalPrefix<ButtonProps>
// 结果:{
// 'app-Label': string;
// 'app-OnClick': () => void;
// 'app-Size': 'small' | 'medium' | 'large'
// }📝 为什么这样设计?
keyof T的作用:遍历原始 Props 的所有属性;as的重命名能力:用模板字面量类型${Prefix}-${Capitalize<K & string>}给属性加前缀,并将首字母大写(符合 HTML 属性的命名规范);- 泛型的灵活性:
Prefix设为默认值'app',也可以根据项目需求修改(比如'admin-')。
🚀 终极合体:用高级类型解决 Pinia 的“Action Payload 推导”
现在我们把 3 条规则合起来,解决Vue3 项目中最常见的需求:
根据 Pinia 的 Action 类型,自动推导对应的 Payload 结构
🌰 实战场景:Pinia 的 User Store
假设我们有一个 User Store,包含 3 个 Action:login(登录)、logout(退出)、updateProfile(更新资料)。我们需要:
- Dispatch Action 时,自动提示对应的 Payload 结构;
- 禁止传入不符合 Action 类型的 Payload。
🔧 实现步骤
1. 定义 Action 类型
// src/stores/user.ts
type UserAction =
| { type: 'user/login'; payload: { username: string; password: string } }
| { type: 'user/logout'; payload: null }
| { type: 'user/updateProfile'; payload: { name: string; email: string } }2. 提取 Action 的 Payload 类型(用条件类型+infer)
// 提取指定Action的Payload类型
type PayloadOfAction<T extends UserAction['type']> = Extract<
UserAction,
{ type: T }
> extends { payload: infer P }
? P
: never3. 在 Pinia Store 中使用
const useUserStore = defineStore('user', {
state: () => ({
/* ... */
}),
actions: {
// 通用Dispatch方法(自动推导Payload类型)
dispatch<T extends UserAction['type']>(
type: T,
payload: PayloadOfAction<T>
) {
switch (type) {
case 'user/login':
// Payload自动推导为{ username: string; password: string }
this.login(payload.username, payload.password)
break
case 'user/logout':
// Payload自动推导为null
this.logout()
break
case 'user/updateProfile':
// Payload自动推导为{ name: string; email: string }
this.updateProfile(payload.name, payload.email)
break
}
},
// 具体Action实现
login(username: string, password: string) {
/* ... */
},
logout() {
/* ... */
},
updateProfile(name: string, email: string) {
/* ... */
},
},
})4. 组件中使用(自动提示 Payload)
const userStore = useUserStore()
// ✅ 正确:login需要{ username, password }
userStore.dispatch('user/login', { username: 'zhangsan', password: '123456' })
// ✅ 正确:logout不需要Payload(payload为null)
userStore.dispatch('user/logout', null)
// ❌ 错误:updateProfile缺少email字段
userStore.dispatch('user/updateProfile', { name: '李四' })📝 用到了哪些规则?
- 条件类型:
Extract<UserAction, { type: T }>从联合类型中提取指定 Action; - infer 提取:
infer P提取 Action 的 Payload 类型; - 裸类型分发:
T extends UserAction['type']自动遍历 Action 类型的联合。
🎯 现在你该怎么做?
TypeScript 的高级类型不是“学了就能用”——你需要把规则“内化”成思维模式。
我的建议是:
- 先搞懂内置工具类型:从
Partial<T>、Required<T>、ReturnType<T>开始,手动实现一遍(比如用Partial<T>实现“将 Props 转为可选”); - 从小需求开始练:比如给你的 Vue3 组件写一个“根据状态动态调整 Props”的类型,或者提取 Pinia Store 的 State 类型;
- 遇到复杂类型先拆解:看到
T extends (infer U)[] ? U : never,先问自己:“这是条件类型吗?用了 infer 吗?是不是在提取数组元素?”
最后想说:高级类型不是为了秀,是为了少写 bug
我曾经以为“会写高级类型”是程序员的“装逼资本”,直到后来:
- 用
NonNullable<T>避免了生产环境的null报错; - 用
ReturnType<T>帮同事找到了函数返回值的类型错误; - 用“Action Payload 推导”减少了 Pinia 中 80%的 payload 拼写错误。
我才明白:高级类型是帮你“在编译时就把 bug 掐死”的工具。
现在回头看,那些曾经让我头皮发麻的类型,其实都是纸老虎。掌握这 3 条规则,你会发现:
TypeScript 的高级类型不是用来吓你的,是帮你更安心写代码的。
你正在卡哪个 TypeScript 类型?
评论区告诉我,我们一起拆解它!如果这篇文章帮你捅破了“高级类型”的窗户纸,点个赞让我知道——你不是一个人在战斗~
- 本文链接:https://fridolph.top/posts/2025-06-02__ts05
- 版权声明:本博客所有文章除特别声明外,均默认采用 CC BY-NC-SA 许可协议。