为什么需要进阶泛型

基础泛型解决”类型参数化”(Array<T>),进阶泛型解决”根据输入类型推导输出类型“。比如:

  • 根据函数拿到返回类型(ReturnType)
  • 根据对象类型挑出部分属性(Pick)
  • 根据数组类型取出元素类型(ElementType)

这些能力都建立在两个核心机制上:条件类型和 infer。

一、条件类型:类型层面的 if/else

1
2
3
4
type IsString<T> = T extends string ? true : false

type A = IsString<'hello'> // true
type B = IsString<123> // false

注意:T extends string 在类型层面不是”继承”,而是”T 是否可以赋值给 string“。

分布式条件类型

当 T 是裸类型参数(没有用 [] 包裹)时,条件类型会”分发”到联合类型的每个成员:

1
2
3
4
5
6
type ToArray<T> = T extends any ? T[] : never

type R = ToArray<string | number>
// 结果:string[] | number[](分发)
// 如果不想分发,用 [T] extends [any] ? T[] : never
// 结果:Array<string | number>(整体)

这个”分发”行为是很多工具类型(如 Exclude)的实现基础:

1
2
3
4
5
// 内置的 Exclude<T, U>:从 T 里剔除 U
type MyExclude<T, U> = T extends U ? never : T

type R = MyExclude<'a' | 'b' | 'c', 'a'>
// 逐成员分发:'a' 剔除,剩 'b' | 'c'

二、infer:从类型里”抽出”一部分

infer 只能在条件类型的 extends 分支里使用,作用是从一个类型结构里推断出某个位置的类型:

1
2
3
4
5
6
7
8
9
10
// 取出数组元素类型
type ElementType<T> = T extends (infer U)[] ? U : never

type A = ElementType<string[]> // string
type B = ElementType<(number)[]> // number

// 取出 Promise 的泛型参数
type Awaited<T> = T extends Promise<infer U> ? U : T

type R = Awaited<Promise<string>> // string

更实战的例子:函数返回类型:

1
2
3
4
5
6
7
8
type MyReturnType<T> = T extends (...args: any[]) => infer R ? R : never

function getUser() {
return { id: 1, name: '张三' }
}

type User = MyReturnType<typeof getUser>
// User = { id: number, name: string }

三、手写常用工具类型

理解了条件类型和 infer,内置工具类型都能自己写出来:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
// Partial:所有属性变可选
type MyPartial<T> = {
[K in keyof T]?: T[K]
}

// Pick:挑出指定属性
type MyPick<T, K extends keyof T> = {
[P in K]: T[P]
}

// Record:以 K 为键、T 为值构造对象
type MyRecord<K extends keyof any, T> = {
[P in K]: T
}

// ReturnType 的完整实现
type MyReturnType<T extends (...args: any) => any> =
T extends (...args: any) => infer R ? R : any

// 递归 + infer:把嵌套数组拍平
type Flatten<T> = T extends (infer U)[]
? U extends any[]
? Flatten<U> // 递归处理嵌套
: U
: T

type R = Flatten<number[][][]> // number

四、条件类型递归:类型层面的循环

条件类型天然支持递归,这是类型体操的核心能力。链式取值:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// 取出深层属性类型
type DeepValue<T, Path extends string> =
Path extends `${infer K}.${infer Rest}`
? K extends keyof T
? DeepValue<T[K], Rest>
: never
: Path extends keyof T
? T[Path]
: never

type User = {
profile: {
address: { city: string }
}
}

type City = DeepValue<User, 'profile.address.city'> // string

模板字符串类型(${infer K}.${infer Rest})让类型也能解析字符串,是路由类型、路径类型的基础。

五、实战:给接口请求写类型安全封装

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// 根据接口路径推导返回类型(路由类型映射)
interface ApiMap {
'/user/info': { id: number; name: string }
'/user/orders': { list: Order[] }
}

type ApiResponse<K extends keyof ApiMap> = Promise<ApiMap[K]>

async function request<K extends keyof ApiMap>(url: K): ApiResponse<K> {
const res = await fetch(url)
return res.json()
}

// 调用时自动获得精确返回类型,不用手写 any
const user = await request('/user/info')
// user: { id: number; name: string }

六、常见坑

  1. any 会破坏条件类型:T extends string 当 T 是 any 时会同时走两个分支,用 unknown 代替;
  2. infer 位置错误:infer 只能出现在 extends 的右侧结构中;
  3. 递归过深报错:TS 对类型递归有深度限制(约 1000 层),复杂递归要注意拆解;
  4. 分布式条件类型踩坑:不确定时用 [T] extends [U] 关掉分发。

总结

进阶泛型的三个台阶:条件类型(分支)→ infer(提取)→ 递归(循环)。日常工作中不追求炫技,但看懂并封装 ReturnType、Pick 这类工具,以及用类型映射约束接口请求,能显著减少 any 和低级错误。