第一篇:重新理解泛型与条件类型 —— 解锁 infer 与分布式条件类型的威力

第一篇:重新理解泛型与条件类型 —— 解锁 infer 与分布式条件类型的威力

为何需要深入泛型?

在日常开发中,我们常写 function identity<T>(arg: T): T 这类基础泛型。但当面对复杂的数据转换、API 响应包装、事件处理等场景时,仅靠简单泛型远远不够。条件类型(Conditional Types)和 infer 关键字能让我们在类型层面做"逻辑判断"和"模式匹配",从而写出自描述、自保护的代码。

条件类型基础:T extends U ? X : Y

条件类型类似于三元表达式,但它作用于类型。例如:

typescript
type IsString<T> = T extends string ? true : false; type A = IsString<'hello'>; // true type B = IsString<number>; // false

关键点在于 分布式条件类型(Distributive Conditional Types):当 T 是一个联合类型(union)时,条件类型会分别应用到每个成员,再将结果联合起来。

typescript
type ToArray<T> = T extends any ? T[] : never; type Result = ToArray<string | number>; // string[] | number[]

这个特性是类型体操的核心武器之一。

infer:在类型中"解构"模式

infer 允许我们在条件类型中声明一个待推断的类型变量,就像在运行时用正则捕获组一样。常见场景:

1. 提取函数返回值类型

typescript
type MyReturnType<T> = T extends (...args: any[]) => infer R ? R : never; function fetchUser() { return { id: 1, name: 'Alice' }; } type User = MyReturnType<typeof fetchUser>; // { id: number; name: string }

注意:这里使用 MyReturnType 而非 ReturnType,避免与 TypeScript 内置工具类型冲突。

2. 提取数组元素类型

typescript
type ElementType<T> = T extends (infer U)[] ? U : never; type Item = ElementType<string[]>; // string

3. 提取 Promise 内部的类型

typescript
type MyAwaited<T> = T extends Promise<infer U> ? U : T; type Data = MyAwaited<Promise<number>>; // number

实战案例:构建类型安全的 API 响应处理器

假设项目中使用统一的 API 响应格式:

typescript
interface ApiResponse<T> { code: number; data: T; message: string; }

我们需要根据 code 的不同值,推导出不同的业务数据类型。例如 code === 0 时返回正常数据,code !== 0data 可能为 null 或错误详情。我们可以利用条件类型和 infer 构建一个"智能解包"类型:

typescript
// 定义几种业务响应形态 type SuccessResponse<T> = { code: 0; data: T; message: 'success' }; type ErrorResponse = { code: number; data: null; message: string }; type ApiResponse<T> = SuccessResponse<T> | ErrorResponse; // 从联合类型中提取成功态的数据类型 // 分布式条件类型会分别处理联合类型的每个成员 type ExtractSuccessData<T> = T extends { code: 0; data: infer D } ? D : never; // 使用示例 type LoginResp = ApiResponse<{ token: string }>; type LoginData = ExtractSuccessData<LoginResp>; // { token: string } // 解析过程:对 SuccessResponse<{token: string}> 提取得到 {token: string} // 对 ErrorResponse 提取得到 never // 最终联合:{token: string} | never = {token: string}

在实际项目中的 API 调用函数:

typescript
async function request<T>(url: string): Promise<T> { // 模拟请求 const raw: ApiResponse<T> = await fetch(url).then(r => r.json()); // 类型守卫,缩小类型 if (raw.code === 0) { // 此处 raw 被推断为 SuccessResponse<T>,data 类型为 T return raw.data; } else { throw new Error(raw.message); } } // 使用时 const user = await request<{ name: string }>('/user'); // user 的类型准确为 { name: string }

进阶:使用 infer 提取函数参数类型并转换

有时我们需要将一个函数的参数类型转换为另一种形式(例如全部变为可选)。利用 infer 和映射类型:

typescript
type MyParameters<T> = T extends (...args: infer P) => any ? P : never; type PartialTuple<T extends any[]> = { [K in keyof T]?: T[K]; }; type PartialParams<T> = T extends (...args: infer P) => infer R ? (...args: PartialTuple<P>) => R : never; function greet(name: string, age: number): string { return `${name} is ${age}`; } type PartialGreet = PartialParams<typeof greet>; // 类型显示为 (args_0?: string | undefined, args_1?: number | undefined) => string

注意PartialTuple 使每个参数变为可选,但调用时仍需要传入正确数量的参数(可以传 undefined),并非真正的"可省略参数"。要实现可变参数数量,需要更复杂的类型体操。

在实践中,如果只是需要适配回调函数,更推荐直接定义新的函数类型,而非过度使用类型体操。

总结与思考

  • 条件类型配合分布式特性,能处理联合类型的过滤与映射。
  • infer 是类型层面的"解构赋值",熟练使用可大幅提升类型抽象能力。
  • 在项目中,可以针对 API 层、事件处理层封装类型工具,减少重复类型定义。
  • 思考:如何用条件类型实现 Exclude<T, U>(排除联合类型中的某些成员)?提示:利用分布式条件类型和 never
返回知识中心