首页 > 解决方案 > 如何在 TypeScript 中键入 autocurry

问题描述

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

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

我发现这篇简洁的文章创建了curry这样的类型:

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类型匹配的实现。

如何为 my 实现类型curryCurry使用该类型的实现看起来如何?

标签: typescriptfunctional-programmingtypescript-typingscurrying

解决方案


这里有一个主要警告: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类型。您可能可以在那里获得更好的类型安全性,但不会太多;编译器将无法验证附加到元组类型末尾所涉及的操作,并且您最终会在任何地方使用类型断言

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

看起来挺好的。


至于如何为完整的可能多次 args 版本进行 TS 输入curry(),它需要类型级元组连接,目前不直接支持(参见 microsoft/TypeScript#5453)。您可以编写一些涉及递归条件类型的东西,但由于递归类型也不受直接支持(请参阅 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>)
        )
      )
    )
  )
);

特别是因为该特定定义使用了可能无法在任何地方都很好地发挥作用的重载。

无论如何,希望有所帮助;祝你好运!

Playground 代码链接


推荐阅读