如何在 TypeScript 中输入 autocurry

How to type an autocurry in TypeScript

我正在尝试为以下函数编写类型:

const curry = (
  f, arr = []
) => (...args) => (
  a => a.length >= f.length ?
    f(...a) :
    curry(f, a)
)([...arr, ...args]);

我发现 this neat articlecurry 创建了一个类型,如下所示:

type Head<T extends any[]> =
  T extends [any, ...any[]]
  ? T[0]
  : never;

type Tail<T extends any[]> =
  ((...t: T) => any) extends ((_: any, ...tail: infer TT) => any)
  ? TT
  : [];

type HasTail<T extends any[]> =
  T extends ([] | [any])
  ? false
  : true;

type Curry<P extends any[], R> = 
  (arg: Head<P>) => HasTail<P> extends true
  ? Curry<Tail<P>, R>
  : R;

我的问题是,此 Curry 类型的签名与我的 curry 函数的签名不匹配(或者我的 TypeScript 技能很差,但确实如此?)。此外,我无法弄清楚如何为 curry 编写与 Curry 类型匹配的实现。

如何为我的 curry 实现类型?使用 Curry 类型的实现看起来如何?

这里有一个主要警告:Function.length 很奇怪;它只会对没有默认参数或剩余参数的函数表现良好。例如,根据您的 TS 编译器针对的 JS 版本,您可能会针对以下代码得到不同的答案:

console.log(((arg = 1) => { }).length); // 0? 1?

因此,当您使用任何依赖于函数参数长度的运行时反射的东西时,请记住这一点。


您的 curry() 函数可分配给 Curry 类型定义,但反之则不行。如果底层函数仍然需要参数,则 Curry 类型定义每次调用时都只需要一个参数,而您的 curry 在调用时将接受任意数量的参数。这意味着我们可以给 curry() 类型 Curry ,但是编译器会限制你调用它的参数数量。它可能看起来像这样:

function curry<P extends any[], R>(f: (...args: P) => R): Curry<Required<P>, R> {
  const _curry = (
    f: (...args: any) => any, arr: any[] = []
  ) => (...args: any) => (
    a => a.length >= f.length ?
      f(...a) :
      _curry(f, a)
  )([...arr, ...args]);
  return _curry(f);
}

在这里,我们基本上放弃了让编译器在实现内部进行任何实际类型检查;里面的一切都或多或少是 any 类型。您可能可以在那里获得更好的类型安全性,但不会太多;编译器将无法验证附加到元组类型末尾所涉及的操作,您最终会在任何地方使用 type assertions

Required<P> 位可能不是必需的;这仅取决于您希望看到带有可选参数的函数发生什么。总的来说,我会非常小心地在联合类型的任何函数上使用它,或者它的参数列表可以有不同的长度等。

让我们确保编译器对正常使用感到满意:

function test(x: string, y: number, z: boolean) {
  return z ? x : y;
}

const t0 = curry(test);
const t1 = t0("abc");
const t2 = t1(123);
const t3 = t2(true); 
console.log(t3); // abc
const t4 = t2(false);
console.log(t4); // 123

看起来不错。


至于如何为 curry() 的一次可能多个参数的完整版本进行 TS 类型化,它需要类型级别的元组连接,目前不直接支持( see microsoft/TypeScript#5453). You can write something that works involving recursive conditional types, but since recursive types are also not directly supported (see microsoft/TypeScript#26980),我不会推荐它们用于生产系统。

或者你可以选择一些最大长度来支持,比如一次三个参数,然后编写一个适用于此的 Curry 版本,但我不知道这是否真的值得:

type Curry3<P extends any[], R> = P extends [] ? R : (
  ((a0: Head<P>) => Curry3<Tail<P>, R>) & (
    P extends [any] ? unknown : (
      ((a0: Head<P>, a1: Head<Tail<P>>) => Curry3<Tail<Tail<P>>, R>) & (
        P extends [any, any] ? unknown : (
          ((a0: Head<P>, a1: Head<Tail<P>>, a2: Head<Tail<Tail<P>>>) =>
            Curry3<Tail<Tail<Tail<P>>>, R>)
        )
      )
    )
  )
);

特别是因为该特定定义使用了 overloads,它可能在任何地方都无法正常播放。

总之,希望对您有所帮助;祝你好运!

Playground link to code