高阶函数与泛型函数的类型体操
高阶函数和泛型是函数式编程的核心,也是 TypeScript 类型系统最强大的部分。掌握这部分内容以后,我们会发现 TypeScript 不仅能检查类型,还能推导类型、组合类型,甚至进行类型运算。
泛型参数约束:给类型参数加"限制条件"
基础泛型约束:确保类型具有某些特性
泛型约束就像给类型参数加上"限制条件",告诉TypeScript:"这个类型参数 T,必须满足某些条件"。这样我们就能在函数体内安全地使用 T 的特定属性。
// 基础约束:T必须具有length属性
interface HasLength {
length: number; // 定义了一个接口,要求有length属性
}
// T extends HasLength 表示:T必须有length属性
function logLength<T extends HasLength>(item: T): void {
// 因为T一定有length,所以可以安全访问
console.log(`长度: ${item.length}`);
}
// ✅ 正确使用
logLength("hello"); // string有length属性
logLength([1, 2, 3]); // 数组有length属性
logLength({ length: 5 }); // 对象有length属性
// ❌ 错误使用
// logLength(42); // number没有length属性 - 编译错误!
// logLength(true); // boolean没有length属性 - 编译错误!
多个约束:必须同时满足多个条件
interface HasId {
id: number;
}
interface HasName {
name: string;
}
// T extends HasId & HasName 表示:T必须同时满足HasId和HasName的要求
function processItem<T extends HasId & HasName>(item: T): string {
// 这里可以安全访问id和name
return `${item.id}: ${item.name}`;
}
// ✅ 必须同时有id和name
processItem({ id: 1, name: "zhangsan", age: 25 }); // ✅ 有id和name,age额外属性不影响
// processItem({ id: 1 }); // ❌ 缺少name - 编译错误!
// processItem({ name: "lisi" }); // ❌ 缺少id - 编译错误!
注:约束只在编译时检查,以确保类型安全。
keyof:更精确的约束
keyof T 可以获取类型T的所有键(属性名)的联合类型,比如:keyof {name: string, age: number}就是 name: string | age: number ,这样可以实现完全类型安全的属性访问。
function getProperty<T, K extends keyof T>(obj: T, key: K): T[K] {
// K extends keyof T 表示:K必须是T的键之一
return obj[key];
}
const person = { name: "zhangsan", age: 25, email: "zhangsan@example.com" };
// ✅ 正确:访问存在的属性
getProperty(person, "name"); // 返回string类型
getProperty(person, "age"); // 返回number类型
// ❌ 错误:访问不存在的属性
// getProperty(person, "address"); // 编译错误:address不是person的键
条件类型约束
条件类型类似于三元表达式,只是适用于类型:T extends U ? X : Y 。如果T可以赋值给U,则类型为X,否则为Y。
type ExtractStringKeys<T> = {
[K in keyof T]: T[K] extends string ? K : never;
}[keyof T];
函数作为参数的类型推导
函数参数的类型推导机制
在 TypeScript 中,当把一个函数作为参数传给另一个函数时,TypeScript 会根据上下文自动推导参数类型。
function mapArray<T, U>(
array: T[], // 第一个参数是T类型的数组
callback: (item: T, index: number) => U // 第二个参数是回调函数
): U[] { // 返回U类型的数组
return array.map(callback);
}
const numbers = [1, 2, 3];
const strings = mapArray(numbers, (n, i) => {
// n自动推导为number,i自动推导为number
return `数字${n}在位置${i}`;
});
上述代码中,TypeScript 的推导过程如下:
- 看到
numbers是number[],所以推导出T = number。 - 在回调函数中,
n自动推导为number,i自动推导为number。 - 回调函数的返回值类型是
string,所以推导出U = string - 因此,整个函数返回值类型是
string[]。
TypeScript可以根据上下文自动推导类型,推导过程是双向的:既可以从左到右推导,也可以从右到左推导。具体的泛型参数
T和U会在调用时被具体化。
高阶函数的类型参数推断
本节开始之前,我们先要明白高阶函数是什么?高阶函数本质就是接受 函数 作为 参数,或 返回函数 作为 结果 的函数。
function compose<A, B, C>(
f: (b: B) => C, // 接受B返回C的函数
g: (a: A) => B // 接受A返回B的函数
): (a: A) => C { // 返回接受A返回C的函数
return (a: A) => f(g(a));
}
// 使用:TypeScript自动推导所有类型
const addOne = (x: number) => x + 1; // number => number
const toString = (x: number) => x.toString(); // number => string
const addOneThenToString = compose(toString, addOne);
// addOneThenToString类型: (x: number) => string
const result = addOneThenToString(5); // "6"
上述代码中,TypeScript 的推导过程如下:
-
const toString = (x: number) => x.toString();接收number类型,并返回string类型,所以compose()函数中f: (b: B) => C:B为number,C为string。 -
const addOne = (x: number) => x + 1;接收number类型,并返回number类型,所以compose()函数中g: (a: A) => B:A为number,B为number。 - 所以最终的推导结果为:
A = number, B = number, C = string。
柯里化函数的类型定义
什么是柯里化函数
柯里化(Currying)函数 其实就是把接受多个参数的函数变成接受一个单一参数(最初函数的第一个参数)的函数,并且返回接受余下的参数且返回结果的新函数的技术。简单来说就是把 f(a, b, c) 变成 f(a)(b)(c) 的函数。
type Curried<A, B, R> = (a: A) => (b: B) => R;
function curry<A, B, R>(fn: (a: A, b: B) => R): Currie2<A, B, R> {
return (a: A) => (b: B) => fn(a, b);
}
const add = (x: number, y: number) => x + y;
const curriedAdd = curry(add);
// 分步调用
const add5 = curriedAdd(5); // 返回新函数:接受一个数,加上5
const result = add5(3); // 8
// 也可以连续调用
const result2 = curriedAdd(5)(3); // 8
自动柯里化:处理任意数量参数
在 TypeScript 中,我们可以用递归类型处理任意数量的参数:判断形参 Args 是否可以进行分解,分解为第一个参数 First 和 剩余参数 Rest。如果可以,则继续采用递归的方式,对剩余参数 Rest 进一步分解,直至无法分解,即只剩下一个参数。
type Curried<Args extends any[], R> =
Args extends [infer First, ...infer Rest]
? (arg: First) => Curried<Rest, R> // 返回接受First的函数,继续处理Rest
: R; // 没有参数了,直接返回结果
我们来看一个简单的代码实例:
function curry<Args extends any[], R>(
fn: (...args: Args) => R
): Curried<Args, R> {
return function curried(...args: any[]): any {
// 如果参数数量够了,就调用原函数
if (args.length >= fn.length) {
return fn(...args as any);
}
// 否则返回新函数,继续收集参数
return (...moreArgs: any[]) => curried(...args, ...moreArgs);
} as any; // 类型断言,因为递归类型比较复杂
}
// 使用:可以柯里化任意参数的函数
const multiply = (a: number, b: number, c: number) => a * b * c;
const curriedMultiply = curry(multiply);
// 可以分任意步调用
const multiplyBy2 = curriedMultiply(2); // 返回:接受两个参数的函数
const multiplyBy2And3 = multiplyBy2(3); // 返回:接受一个参数的函数
const finalResult = multiplyBy2And3(4); // 24
// 也可以连续调用
const finalResult2 = curriedMultiply(2)(3)(4); // 24
函数组合的类型体操
管道(Pipe)操作
所谓 管道,就是把多个函数连接起来,用前一个函数的输出作为后一个函数的输入。
管道定义
function pipe<F extends [(...args: any[]) => any, ...Array<(arg: any) => any>]>(
...functions: F
): (...args: Parameters<F[0]>) => ReturnType<F[F['length'] - 1]> {
return (...args: Parameters<F[0]>) => {
let result = functions[0](...args); // 执行第一个函数
for (let i = 1; i < functions.length; i++) {
result = functions[i](result); // 用前一个结果调用下一个函数
}
return result;
} as any;
}
上述代码中,F是函数数组,第一个函数接受任意参数,后续函数接受前一个函数的返回值。
管道使用
const add = (x: number) => x + 1;
const multiply = (x: number) => x * 2;
const toString = (x: number) => x.toString();
// 创建处理管道
const process = pipe(add, multiply, toString);
const result = process(5);
console.log(result); // "12"
上述代码的输出结果是 "12" ,我们用直观一点的理解就是:process(5) 相当于 toString(multiply(add(5))) 。
组合(Compose)操作
组合 操作正好与 管道 操作相反,它是从右到左组合函数。
组合定义
function compose<Functions extends [(arg: any) => any, ...Array<(arg: any) => any>]>(
...functions: Functions
): (arg: any) => ReturnType<Functions[0]> {
return (arg: any) => {
let result = arg;
// 从右向左执行
for (let i = functions.length - 1; i >= 0; i--) {
result = functions[i](result);
}
return result;
} as any;
}
组合使用
const toUpper = (s: string) => s.toUpperCase();
const addExclamation = (s: string) => s + "!";
const repeat = (s: string) => s + s;
// 从右到左组合:repeat → addExclamation → toUpper
const shout = compose(toUpper, addExclamation, repeat);
const shouted = shout("hello");
console.log(shouted); // "HELLOHELLO!"
上述代码的输出结果是:"HELLOHELLO!" 其执行过程如下:
-
shout("hello")函数会先调用repeat()函数,输出结果是:"hellohello"。 - 接着再调用
addExclamation()函数,输出结果是:"hellohello!"。 - 最后再调用
toUpper()函数,输出结果是:"HELLOHELLO!"。
异步函数组合
当函数返回Promise时,我们需要特殊的组合方式。
异步管道的简单实现
async function asyncPipe<Functions extends Array<(arg: any) => any>>(
...functions: Functions
): (input: any) => Promise<any> {
return async (input: any) => {
let result = await Promise.resolve(input); // 处理可能是Promise的输入
for (const fn of functions) {
result = await Promise.resolve(fn(result)); // 等待每个函数完成
}
return result;
};
}
异步管道的简单实例
const fetchUser = async (id: number) => {
console.log(`获取用户${id}...`);
return { id, name: "User" + id };
};
const processUser = (user: any) => {
console.log("处理用户...");
return { ...user, processed: true };
};
const saveUser = async (user: any) => {
console.log("保存用户...");
return user;
};
// 创建异步管道:fetchUser → processUser → saveUser
const userPipeline = asyncPipe(fetchUser, processUser, saveUser);
// 执行管道
userPipeline(1).then(user => {
console.log("最终用户:", user);
});
结语
类型体操的目的是让代码更安全、更清晰,而不是炫耀技术。我们应该从实际需求出发,选择最简单的解决方案。
对于文章中错误的地方或者有任何问题,欢迎在评论区留言讨论!