0%

TypeScript 实现通用函数柯里化(支持类型推导)

柯里化(Currying)是函数式编程中的一个重要概念,它指的是将一个多参数函数转换成一系列接收单个参数的函数,每次调用只处理一个参数,直到接收完所有参数并执行函数。

在 JavaScript 中,lodash 等函数工具库提供了 _.curry 方法。但今天我们不使用库,而是用 TypeScript 实现一个具备完整类型推导的通用 curry 函数

graph TD
  A[原始函数] --> B[函数定义]
  A --> C[参数传递流程]
  B --> D["function f(a: A, b: B, c: C)"]
  C --> E["a: A"]
  C --> F["(b: B)"]
  C --> G["(c: C)"]

🔍 使用场景举例

举个例子,如果我们有这样一个函数:

1
2
3
4
5
6
7
8
9
10
11
12
13
function a(age: number, name: string, gender: 0 | 1) {
return `${name} is ${age} years old, gender: ${gender === 0 ? 'Female' : 'Male'}`;
}
````

我们希望可以这样调用它:

```ts
const curried = curry(a);

const step1 = curried(23); // 推导为 (name: string) => ...
const step2 = step1('Alice'); // 推导为 (gender: 0 | 1) => ...
const result = step2(0); // 最终 result 是 string 类型

✅ 最终实现

这段代码的关键有两个部分:

  1. 类型定义:我们要让 TypeScript 能够自动推导每一层参数的类型;
  2. 运行时逻辑:要在每次调用时判断参数是否收集完,若未完成,则返回下一个函数。

完整代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// 类型定义
type Curried<A extends any[], R> =
A extends [] ? () => R :
A extends [infer F] ? (x: F) => R :
A extends [infer F, ...infer Rest] ? (x: F) => Curried<Rest, R> :
never;

// 函数实现
function curry<A extends any[], R>(fn: (...args: A) => R): Curried<A, R> {
return function curried(...args: any[]): any {
if (args.length >= fn.length) {
return fn(...args as A);
} else {
return (...next: any[]) => curried(...args, ...next);
}
} as Curried<A, R>;
}

注意:as Curried<A, R> 是关键的一步类型断言,用于让最终返回值拥有正确的类型提示。


🧠 类型推导原理解析

我们来看 Curried 的类型定义:

1
2
3
4
5
type Curried<A extends any[], R> = 
A extends [] ? () => R :
A extends [infer F] ? (x: F) => R :
A extends [infer F, ...infer Rest] ? (x: F) => Curried<Rest, R> :
never;
  • A 是一个参数数组类型,例如 [number, string, boolean]
  • 如果是空数组,返回 () => R
  • 如果是一个参数,返回 (x: 参数类型) => R
  • 否则递归返回 (x: 第一个参数) => 柯里化剩下的参数

这就实现了类型层面上的“参数分发”。


🚀 使用示例

我们再回头看看最初的示例,看看 TypeScript 能否正确推导类型:

1
2
3
4
5
6
7
8
9
10
11
function a(age: number, name: string, gender: 0 | 1) {
return `${name} is ${age} years old, gender: ${gender === 0 ? 'Female' : 'Male'}`;
}

const curried = curry(a);

const fn1 = curried(18); // 推导为 (name: string) => ...
const fn2 = fn1('Bob'); // 推导为 (gender: 0 | 1) => ...
const result = fn2(1); // 推导为 string

console.log(result); // "Bob is 18 years old, gender: Male"

TypeScript 全程都能正确提示参数类型 ✅。


🧩 补充思考:柯里化的意义?

柯里化的一个常见用途是参数复用函数组合

1
2
3
4
const withAge18 = curried(18);
const withAge18AndNameTom = withAge18('Tom');

console.log(withAge18AndNameTom(0)); // "Tom is 18 years old, gender: Female"

这种“预设部分参数”的方式,在表单处理、配置函数、事件绑定等场景中很有用。


🧱 总结

  • 我们实现了一个支持完整类型推导的 curry 函数;
  • 利用了 TypeScript 的条件类型、递归类型等高级技巧;
  • 在多个参数的函数中,也能逐层传参并得到良好的类型提示;
  • 柯里化是函数式编程的重要组成部分,在实际开发中也有不少落地场景。

如果你觉得有用,不妨点赞 + 收藏,一起探索更多 TypeScript 魔法 ✨