第一篇:重新理解泛型与条件类型 —— 解锁 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 !== 0 时 data 可能为 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。