5 个复杂泛型的拆解思路,教你从看不懂到写得出!
📌 点赞收藏关注不迷路!
你是否在 TypeScript 的源码、第三方库里看过这样的泛型:
type DeepPartial<T> = {
[P in keyof T]?: DeepPartial<T[P]>
}
或者这样的:
type Flatten<T> = T extends (infer U)[] ? U : T
是不是一看就觉得“这都是什么天书”?别怕!
这篇文章带你掌握 5 个常见复杂泛型的拆解思路与实战案例,从看不懂到能写出,一套搞定!
🧠 总体思路:拆解复杂泛型的 5 步
无论泛型多复杂,我们都可以按这 5 步来拆解它:
步骤 | 说明 |
---|---|
1️⃣ 泛型参数:泛型变量 T 是谁? | |
2️⃣ 条件判断:有没有 extends 条件判断? | |
3️⃣ 推断结构:是否用 infer 推导? | |
4️⃣ 映射类型:是否用了 keyof / in 遍历? | |
5️⃣ 递归调用:是否对自己递归调用? |
🎯 案例 1:DeepPartial — 递归让对象变“半透明”
type DeepPartial<T> = {
[P in keyof T]?: DeepPartial<T[P]>
}
💡 这是做什么的?
将对象中所有属性都变为可选,并且嵌套对象也一样可选!
🧩 拆解思路:
步骤 | 解释 |
---|---|
1️⃣ 泛型参数 T | 任意类型,如对象 { a: string; b: { c: number } } |
4️⃣ 映射类型 [P in keyof T] | 遍历 T 的所有属性 |
5️⃣ 递归调用 DeepPartial<T[P]> | 对属性值再次应用该类型 |
➕ 可选符号 ? | 将每个属性设为可选 |
✅ 使用效果:
type User = {
name: string
profile: {
age: number
city: string
}
}
type OptionalUser = DeepPartial<User>
// 相当于:
/*
{
name?: string
profile?: {
age?: number
city?: string
}
}
*/
🎯 案例 2:Flatten — 从数组中提取基础类型
type Flatten<T> = T extends (infer U)[] ? U : T
💡 这是做什么的?
如果传入的是数组类型,比如 string[]
,就返回 string
;否则原样返回。
🧩 拆解思路:
步骤 | 解释 |
---|---|
1️⃣ 泛型 T | 任意类型,比如 string[] 、number |
2️⃣ 条件判断 extends | 判断是否为数组类型 |
3️⃣ 推导 infer U | 推导出数组中元素类型 |
结果 | 如果是数组就提取,否则原样返回 |
✅ 使用效果:
type A = Flatten<number[]> // number
type B = Flatten<string> // string
🎯 案例 3:PickByType — 挑出类型为某种类型的属性
type PickByType<T, U> = {
[K in keyof T as T[K] extends U ? K : never]: T[K]
}
💡 这是做什么的?
从对象中筛选出类型为 U 的字段。
🧩 拆解思路:
步骤 | 解释 |
---|---|
1️⃣ 泛型参数 T/U | T 是原对象类型,U 是目标类型 |
4️⃣ 映射类型 [K in keyof T as ...] | 遍历属性并用 as 改名或剔除 |
2️⃣ 条件判断 T[K] extends U | 如果该属性是目标类型就保留 |
else → never | 被映射为 never 的键会被丢弃 |
✅ 使用效果:
type Example = {
id: number
name: string
age: number
}
type OnlyNumbers = PickByType<Example, number>
// => { id: number; age: number }
🎯 案例 4:UnionToIntersection — 联合转交叉类型
type UnionToIntersection<U> =
(U extends any ? (arg: U) => any : never) extends (arg: infer R) => any
? R
: never
💡 这是做什么的?
将联合类型变成交叉类型:
type A = { a: string }
type B = { b: number }
type U = A | B
type I = UnionToIntersection<U> // { a: string } & { b: number }
🧩 拆解思路:
步骤 | 解释 |
---|---|
1️⃣ 泛型参数 U | 是联合类型 A | B |
2️⃣ 条件判断 U extends any | 利用分布式条件类型,逐一取出 A/B |
3️⃣ infer R 提取参数类型 | 推导出合并后的参数类型 |
Trick | 利用函数参数的交叉特性,得到交叉类型 |
📌 这是一个 高级技巧,核心是:
TS 函数参数是逆变的,所以可以用来“合并”联合类型!
🎯 案例 5:IsAny — 判断某个类型是否是 any
type IsAny<T> = 0 extends (1 & T) ? true : false
💡 这是做什么的?
这是判断一个类型是否为 any
的方法,TS
原生不提供,需用技巧实现。
🧩 拆解思路:
步骤 | 解释 |
---|---|
利用交叉类型:1 & T | 如果 T 是 any ,则 1 & any 是 any |
判断 0 extends any | 永远为 true,因此结果是 true |
其他类型不会满足条件 | 所以返回 false |
✅ 使用效果:
type A = IsAny<any> // true
type B = IsAny<string> // false
type C = IsAny<unknown> // false
🧠 总结:复杂泛型,其实也讲“套路”
泛型写法 | 技术点 | 关键词 |
---|---|---|
extends | 类型判断 | 条件判断 |
infer | 推导类型变量 | 函数参数、数组、返回值 |
keyof / in | 遍历属性 | 映射类型 |
递归引用 | 处理嵌套结构 | Tree / JSON |
as + never | 过滤属性名 | Pick/Omit 高阶版本 |
掌握这些套路,你不仅能看懂源码中奇怪的泛型,甚至能自己写出来!
✅ 最后说一句
如果你看完这篇文章感觉自己对复杂泛型没那么害怕了,请给我点个 点赞 + 收藏 + 关注 吧!