泛型:TypeScript 的类型编程艺术
前言:当类型注释不再够用
在 TypeScript 的世界里,大多数情况下我们只是在做"类型注释"——告诉编译器某个变量是什么类型。但当遇到复杂场景时,简单的类型注释就像用记事本写代码一样无力。这时候,泛型登场了。
场景一:函数柯里化的类型魔法
问题:没有泛型的类型黑洞
// 尝试写一个柯里化函数,结果...
function curry(fn: any): any {
return function curried(...args: any[]) {
if (args.length >= fn.length) {
return fn(...args);
} else {
return function(...moreArgs: any[]) {
return curried(...args, ...moreArgs);
};
}
};
}
const add = (a: number, b: number) => a + b;
const curriedAdd = curry(add);
// 灾难:完全丢失类型安全!
const step1 = curriedAdd(1); // any
const result = curriedAdd(1)(2); // any
curriedAdd(1)("hello"); // 运行时才报错!
解决方案:泛型类型编程
// 泛型实现的类型安全柯里化
type Curried<T extends (...args: any[]) => any> =
T extends (...args: infer Args) => infer Return
? Args extends [infer First, ...infer Rest]
? Rest extends []
? (arg: First) => Return
: (arg: First) => Curried<(...args: Rest) => Return>
: () => Return
: never;
function curry<T extends (...args: any[]) => any>(fn: T): Curried<T> {
return function curried(...args: any[]): any {
if (args.length >= fn.length) {
return fn(...args);
} else {
return function(...moreArgs: any[]) {
return curried(...args, ...moreArgs);
};
}
};
}
// 使用体验:完美类型推断
const add = (a: number, b: number, c: number) => a + b + c;
const curriedAdd = curry(add);
// ✅ 每一步都有精确类型
const step1 = curriedAdd(1); // (b: number) => (c: number) => number
const step2 = curriedAdd(1)(2); // (c: number) => number
const result = curriedAdd(1)(2)(3); // number
// ✅ 多种调用方式都类型安全
const result1 = curriedAdd(1, 2)(3); // number
const result2 = curriedAdd(1, 2, 3); // number
// ❌ 错误在编写时立即发现
curriedAdd(1)("2"); // 编译错误:类型不匹配
核心价值:泛型让我们能够表达"根据输入函数的类型,动态推导出柯里化后函数的类型"这种复杂逻辑。
场景二:深度 Partial 的递归力量
问题:内置工具的局限性
interface User {
id: number;
profile: {
name: string;
address: {
city: string;
street: string;
};
};
}
// 内置 Partial 只能处理第一层
type ShallowPartial = Partial<User>;
const updateData: ShallowPartial = {
profile: {
name: "新名字",
address: { // ❌ 这里 address 还是必须的!
city: "北京",
street: "必须填写" // 必须传 street!
}
}
};
解决方案:递归泛型类型
// 深度 Partial:不用泛型根本写不出来
type DeepPartial<T> = {
[P in keyof T]?: T[P] extends object ? DeepPartial<T[P]> : T[P];
};
type PartialUser = DeepPartial<User>;
// 现在可以优雅地部分更新了!
const updateData: PartialUser = {
profile: {
name: "新名字",
address: {
city: "北京"
// street 可以不传!
}
}
// id 也可以不传!
};
// 支持无限层级嵌套
const minimalUpdate: PartialUser = {
profile: {
address: {} // 只更新到 address 层级
}
};
核心价值:泛型让我们能够定义递归的类型变换规则,这是普通类型注解无法做到的。
场景三:函数参数的类型提取
问题:手动维护的类型同步
function createUser(name: string, age: number, email: string): User {
// ...
}
// 手动提取参数类型(容易忘记更新)
type CreateUserParams = [string, number, string];
// 当函数签名改变时...
function createUser(name: string, age: number, email: string, role: string): User {
// ...
}
// 必须记得手动更新类型(但经常会忘!)
type CreateUserParams = [string, number, string, string]; // 容易漏改
解决方案:泛型类型提取
// 一次定义,自动同步
type FunctionParams<T extends (...args: any[]) => any> =
T extends (...args: infer P) => any ? P : never;
function createUser(name: string, age: number, email: string): User {
// ...
}
// 自动提取参数类型
type CreateUserParams = FunctionParams<typeof createUser>; // [string, number, string]
// 当函数签名改变时...
function createUser(name: string, age: number, email: string, role: string): User {
// ...
}
// 类型自动更新!
type CreateUserParams = FunctionParams<typeof createUser>; // [string, number, string, string]
// 实际应用:类型安全的高阶函数
function withLogging<T extends (...args: any[]) => any>(fn: T): T {
return function(...args: FunctionParams<T>): ReturnType<T> {
console.log('调用参数:', args);
return fn(...args);
} as T;
}
const loggedCreateUser = withLogging(createUser);
// ✅ 完全类型安全!
loggedCreateUser("张三", 25, "zhang@example.com", "admin"); // 正确
loggedCreateUser("李四", "30", "li@example.com"); // 错误:参数类型不匹配
核心价值:泛型让我们能够从现有类型中提取和操作类型信息,实现类型的自动同步。
场景四:条件类型分发
问题:类型逻辑的丢失
function processValue(input: string | number | boolean) {
if (typeof input === 'string') {
return input.length; // number
} else if (typeof input === 'number') {
return input.toString(); // string
} else {
return !input; // boolean
}
}
// 返回类型怎么写?只能丢失信息
function processValue(input: string | number | boolean): any {
// ...
}
// 使用时的痛苦
const result1 = processValue("hello"); // any,不知道是 number
const result2 = processValue(42); // any,不知道是 string
解决方案:条件类型分发
// 泛型条件类型:精确的类型映射
type ProcessResult<T> =
T extends string ? number :
T extends number ? string :
T extends boolean ? boolean :
never;
function processValue<T extends string | number | boolean>(input: T): ProcessResult<T> {
if (typeof input === 'string') {
return input.length as ProcessResult<T>;
} else if (typeof input === 'number') {
return input.toString() as ProcessResult<T>;
} else {
return !input as ProcessResult<T>;
}
}
// 使用:完美的类型推断!
const result1 = processValue("hello"); // number
const result2 = processValue(42); // string
const result3 = processValue(true); // boolean
// 联合类型的自动分发
const inputs = ["hello", 42, true] as const;
const results = inputs.map(x => processValue(x));
// 类型: (number | string | boolean)[]
// 知道第一个是 number,第二个是 string,第三个是 boolean
核心价值:泛型让我们能够根据输入类型动态决定输出类型,实现真正的类型级编程。
场景五:类型安全的 Promise 链
问题:Promise 链中的类型丢失
async function fetchUser(id: string): Promise<User> { /* ... */ }
async function validateUser(user: User): Promise<boolean> { /* ... */ }
async function sendEmail(user: User): Promise<void> { /* ... */ }
// 传统的 Promise 链:类型信息逐渐模糊
fetchUser("123")
.then(user => validateUser(user)) // Promise<boolean>
.then(isValid => {
if (isValid) {
return fetchUser("123").then(user => sendEmail(user)); // Promise<any>
}
return false; // 这里类型已经混乱了
})
.then(result => {
// result 是什么类型?谁知道呢!
});
解决方案:泛型 Promise 流水线
// 类型安全的 Promise 流水线
type PromisePipe<T> = {
then: <U>(fn: (value: T) => U | Promise<U>) => PromisePipe<U>;
result: () => Promise<T>;
};
function createPromisePipe<T>(promise: Promise<T>): PromisePipe<T> {
return {
then<U>(fn: (value: T) => U | Promise<U>): PromisePipe<U> {
const newPromise = promise.then(fn);
return createPromisePipe(newPromise);
},
result: () => promise
};
}
// 使用:完美的类型流转
const result = await createPromisePipe(fetchUser("123"))
.then(user => validateUser(user)) // 知道这里返回 boolean
.then(isValid => {
if (isValid) {
return fetchUser("123")
.then(user => sendEmail(user)) // 知道这里返回 void
.then(() => "邮件发送成功");
}
return "验证失败";
}) // 知道这里返回 string
.result();
// result 的类型是 string,TypeScript 完全知道!
总结:泛型的本质是类型编程
普通类型注释就像给变量贴标签:
const name: string = "张三";
const age: number = 25;
泛型类型编程就像编写操作类型的函数:
// 这是一个"类型函数":输入一个类型 T,输出一个深度可选的类型
type DeepPartial<T> = {
[P in keyof T]?: T[P] extends object ? DeepPartial<T[P]> : T[P];
};
什么时候必须用泛型?
当你有以下需求时,泛型是唯一选择:
- 类型级计算:需要对类型进行递归、条件判断等操作
- 类型关系保持:需要维护输入类型与输出类型之间的关系
- 动态类型推断:需要根据使用时的具体类型动态确定其他类型
- 类型信息提取:需要从现有类型结构中提取部分信息
开始你的类型编程之旅
从最简单的泛型工具开始:
// 工具1:让所有属性变为只读
type ReadonlyDeep<T> = {
readonly [P in keyof T]: T[P] extends object ? ReadonlyDeep<T[P]> : T[P];
};
// 工具2:从联合类型中提取函数类型
type FunctionMembers<T> = {
[K in keyof T]: T[K] extends (...args: any[]) => any ? T[K] : never;
}[keyof T];
// 工具3:获取 Promise 的包裹类型
type UnwrapPromise<T> = T extends Promise<infer U> ? U : T;
记住:当你发现自己在重复写相似的类型定义,或者需要表达复杂的类型关系时,就是泛型登场的最佳时机!
泛型不是让代码变复杂的元凶,而是让你从重复的类型劳动中解放的利器。开始尝试类型编程,你会发现 TypeScript 的真正威力!