TypeScript 3的通用咖喱功能

wag*_*eld 10 generics currying typescript

TypeScript 3.0引入了通用的rest参数.

到目前为止,curry函数必须在TypeScript中注释,其中包含有限数量的函数重载和一系列条件语句,用于查询实现中传递的参数的数量.

我希望通用的rest参数最终提供实现完全通用解决方案所需的机制.

我想知道如何使用这种新的语言功能来编写通用curry函数...假设它当然可能!

使用rest params的JS实现,我在hackernoon上找到解决方案中稍微修改了一下,看起来像这样:

function curry(fn) {
  return (...args) => {
    if (args.length === 0) {
      throw new Error("Empty invocation")
    } else if (args.length < fn.length) {
      return curry(fn.bind(null, ...args))
    } else {
      return fn(...args)
    }
  }
}
Run Code Online (Sandbox Code Playgroud)

使用泛型休息参数和函数重载,我curry在TypeScript 中注释此函数的尝试如下所示:

interface CurriedFunction<T extends any[], R> {
  (...args: T): void // Function that throws error when zero args are passed
  (...args: T): CurriedFunction<T, R> // Partially applied function
  (...args: T): R // Fully applied function
}

function curry<T extends any[], R>(
  fn: CurriedFunction<T, R>
): CurriedFunction<T, R> {
  return (...args: T) => {
    if (args.length === 0) {
      throw new Error("Empty invocation")
    } else if (args.length < fn.length) {
      return curry(fn.bind(null, ...args))
    } else {
      return fn(...args)
    }
  }
}
Run Code Online (Sandbox Code Playgroud)

但是TypeScript会抛出错误:

Type 'CurriedFunction<any[], {}>' is not assignable to type 'CurriedFunction<T, R>'.
Type '{}' is not assignable to type 'R'.
Run Code Online (Sandbox Code Playgroud)

我不明白R推断的地点和原因是{}什么?

我们非常感谢您对TypeScript众神的任何帮助.

jca*_*alz 6

现在,正确键入此字符的最大障碍是TypeScript从TypeScript 3.0开始无法连接或拆分元组。有一些建议可以做到这一点,TypeScript 3.1及更高版本可能正在开发中,但现在还不存在。截至今天你可以做的是枚举的情况下达到最大的一些有限长度,或尝试编译器欺骗使用递归这是不推荐使用

如果我们想象有一个TupleSplit<T extends any[], L extends number>类型函数可以接受一个元组和一个长度并将该长度的元组分成初始组件和其余部分,从而TupleSplit<[string, number, boolean], 2>产生{init: [string, number], rest: [boolean]},那么您可以将curry函数的类型声明为如下形式:

declare function curry<A extends any[], R>(
  f: (...args: A) => R
): <L extends TupleSplit<A, number>['init']>(
    ...args: L
  ) => 0 extends L['length'] ?
    never :
    ((...args: TupleSplit<A, L['length']>['rest']) => R) extends infer F ?
    F extends () => any ? R : F : never;
Run Code Online (Sandbox Code Playgroud)

为了能够尝试着想,让我们介绍的一个版本TupleSplit<T, L>是只对L3(你可以添加到,如果你想)。看起来像这样:

type TupleSplit<T extends any[], L extends number, F = (...a: T) => void> = [
  { init: [], rest: T },
  F extends ((a: infer A, ...z: infer Z) => void) ?
  { init: [A], rest: Z } : never,
  F extends ((a: infer A, b: infer B, ...z: infer Z) => void) ?
  { init: [A, B], rest: Z } : never,
  F extends ((a: infer A, b: infer B, c: infer C, ...z: infer Z) => void) ?
  { init: [A, B, C], rest: Z } : never,
  // etc etc for tuples of length 4 and greater
  ...{ init: T, rest: [] }[]
][L];
Run Code Online (Sandbox Code Playgroud)

现在我们可以curry在类似的函数上测试该声明

function add(x: number, y: number) {
  return x + y;
}
const curriedAdd = curry(add);

const addTwo = curriedAdd(2); // (y: number) => number;
const four = curriedAdd(2,2); // number
const willBeAnError = curriedAdd(); // never
Run Code Online (Sandbox Code Playgroud)

这些类型在我看来是正确的。


当然,这并不意味着将实施curry将竭诚与该类型。您也许可以像这样实现它:

return <L extends TupleSplit<A, number>['init']>(...args: TupleSplit<A, L['length']>['rest']) => {
  if (args.length === 0) {
    throw new Error("Empty invocation")
  } else if (args.length < f.length) {
    return curry(f.bind(null, ...args))
  } else {
    return f(...args as A)
  }
}
Run Code Online (Sandbox Code Playgroud)

可能的。我还没有测试过。

无论如何,希望这有意义并能给您一些指导。祝好运!


更新

curry()如果您不传递所有参数,则我不会注意返回进一步的咖喱函数的事实。这样做需要递归类型,如下所示:

type Curried<A extends any[], R> =
  <L extends TupleSplit<A, number>['init']>(...args: L) =>
    0 extends L['length'] ? never :
    0 extends TupleSplit<A, L['length']>['rest']['length'] ? R :
    Curried<TupleSplit<A,L['length']>['rest'], R>;

declare function curry<A extends any[], R>(f: (...args: A)=>R): Curried<A, R>;

function add(x: number, y: number) {
  return x + y;
}
const curriedAdd = curry(add);

const addTwo = curriedAdd(2); // Curried<[number], number>
const three = addTwo(1); // number
const four = curriedAdd(2,2); // number
const willBeAnError = curriedAdd(); // never
Run Code Online (Sandbox Code Playgroud)

那更像是原始定义。


但我也注意到,如果您这样做:

const wat = curriedAdd("no error?"); // never
Run Code Online (Sandbox Code Playgroud)

而不是得到错误,而是返回never。在我看来,这似乎是编译器错误,但我还没有跟进。编辑:好的,我对此提交了Microsoft / TypeScript#26491

干杯!

  • `X 扩展 Y ? T : U` 语法称为 [条件类型](https://www.typescriptlang.org/docs/handbook/release-notes/typescript-2-8.html#conditional-types)(`?` 和 ` :` 也是子句的一部分,而不仅仅是 `extends` 部分)。如果 `L` 是具有名为 `length` 的属性的类型(例如元组),则 `L['length']` 是该属性的类型。该符号称为 [查找类型](https://www.typescriptlang.org/docs/handbook/release-notes/typescript-2-1.html#keyof-and-lookup-types)。所以`0 extends L['length']` 是条件类型的一部分,检查`L` 是否是一个零长度元组。 (2认同)

Pet*_*ete 6

使用当前版本的 typescript,可以创建一个相对简单、类型正确的通用 curry 函数。

type CurryFirst<T> = T extends (x: infer U, ...rest: any) => any ? U : never;
type CurryRest<T> =
    T extends (x: infer U) => infer V ? U :
    T extends (x: infer U, ...rest: infer V) => infer W ? Curried<(...args: V) => W> :
    never

type Curried<T extends (...args: any) => any> = (x: CurryFirst<T>) => CurryRest<T>

const curry = <T extends (...args: any) => any>(fn: T): Curried<T> => {
    if (!fn.length) { return fn(); }
    return (arg: CurryFirst<T>): CurryRest<T> => {
        return curry(fn.bind(null, arg) as any) as any;
    };
}

describe("Curry", () => {
    it("Works", () => {
        const add = (x: number, y: number, z: number) => x + y + z;
        const result = curry(add)(1)(2)(3)
        result.should.equal(6);
    });
});
Run Code Online (Sandbox Code Playgroud)

这是基于两个类型构造函数:

  • CurryFirst会给一个函数返回该函数的第一个参数的类型。
  • CurryRest将返回应用第一个参数的柯里化函数的返回类型。特殊情况是当函数类型T只接受一个参数时,那么CurryRest<T>将只返回该函数类型的返回类型T

基于这两者,类型函数的柯里化版本的类型签名T简单地变为:

Curried<T> = (arg: CurryFirst<T>) => CurryRest<T>
Run Code Online (Sandbox Code Playgroud)

我在这里做了一些简单的限制:

  • 您不想柯里化无参数函数。您可以轻松添加它,但我认为这没有意义。
  • 我不保留this指针。这对我来说也没有意义,因为我们正在进入纯 FP 领域。

如果 curry 函数将参数累积在数组中并执行一次fn.apply而不是多次fn.bind调用,则可以实现推测性能改进。但必须注意确保部分应用的函数可以被正确调用多次。