Search code examples
typescriptgenericscurryingtypescript3.0

Generic curry function with TypeScript 3


TypeScript 3.0 introduced generic rest parameters.

Up until this point, curry functions had to be annotated in TypeScript with a finite number of function overloads and a series of conditional statements querying the number of passed arguments within the implementation.

I am hoping that generic rest parameters finally offer the mechanism needed to implement a completely generic solution.

I would like to know how to use this new language feature to write a generic curry function... assuming it's possible of course!

The JS implementation using rest params that I modified a little from a solution I found on hackernoon looks like this:

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)
    }
  }
}

Using generic rest params and function overloads, my attempt at annotating this curry function in TypeScript looks like this:

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)
    }
  }
}

However TypeScript throws the error:

Type 'CurriedFunction<any[], {}>' is not assignable to type 'CurriedFunction<T, R>'.
Type '{}' is not assignable to type 'R'.

I don't understand where and why R is being inferred as {}?


Solution

  • Right now the biggest hurdle for typing this correctly is TypeScript's inability to concatenate or split tuples as of TypeScript 3.0. There are suggestions for doing this, and something might be in the works for TypeScript 3.1 and beyond, but it's just not there right now. As of today all you could do is enumerate cases up to some maximum finite length, or try to trick the compiler into using recursion which is not recommended.

    If we imagine that there were a TupleSplit<T extends any[], L extends number> type function which could take a tuple and a length and split the tuple at that length into the initial component and the rest, so that TupleSplit<[string, number, boolean], 2> would produce {init: [string, number], rest: [boolean]}, then you could declare your curry function's type as something like this:

    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;
    

    For the sake of being able to try that, let's introduce a version of TupleSplit<T, L> that only works for L up to 3 (which you can add to if you want). It looks like this:

    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];
    

    Now we can test that declaration of curry on a function like

    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
    

    Those types look correct to me.


    Of course, that doesn't mean the implementation of curry will be happy with that type. You might be able to implement it like:

    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)
      }
    }
    

    possibly. I haven't tested that.

    Anyway, hope that makes some sense and gives you some direction. Good luck!


    UPDATE

    I didn't pay attention to the fact that curry() returns further curried functions, if you don't pass in all the arguments. Doing that requires a recursive type, like this:

    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
    

    That's more like the original definition.


    But I also notice that if you do this:

    const wat = curriedAdd("no error?"); // never
    

    that instead of getting an error, it returns never. This looks like a compiler bug to me, but I haven't followed it up yet. EDIT: Okay, I filed Microsoft/TypeScript#26491 about this.

    Cheers!