Skip to content

Instantly share code, notes, and snippets.

@donnut
Last active October 28, 2023 17:58
Show Gist options
  • Star 68 You must be signed in to star a gist
  • Fork 10 You must be signed in to fork a gist
  • Save donnut/fd56232da58d25ceecf1 to your computer and use it in GitHub Desktop.
Save donnut/fd56232da58d25ceecf1 to your computer and use it in GitHub Desktop.
TypeScript and currying

TypeScript and currying

In functional programming you often want to apply a function partly. A simple example is a function add. It would be nice if we could use add like:

var res2 = add(1, 3); // => 4

var add10To = add(10);
var res = add10To(5); // => 15

In the first line add is used by providing two arguments and the function can add them and return the result. The second part shows a function add10To that is the result of calling add with only one argument. So add returns a function that expects the second argument at a later stage.

How do we implement this currying of functions in TypeScript?

Lets start with the signature of add:

add:: number -> number -> number

This is straight forward, but now we need to implement it such that add returns a function when it receives only one argument. In terms of signatures, this would be:

add:: number -> (number -> number) // calling add with one argument returns a function

The following implementation services this purpose:

function add(x: number): (y: number) => number {
    return function(y: number): number {
        return x + y;
    }
}

This works fine to create add10To, but forces us to specify add(1)(3) instead of the more appropriate add(1, 3). Look at the diffent ways of calling add. Not that the res* variables get their type set explicitly.

var res1: Function = add(1);
var res2: (y: number) => number = add(1);
var res3: number = add(1)(3);
var res4: number = add(1, 3); // compile error

The last line generates a compile error:

curry.ts(46,20): error TS2346: Supplied parameters do not match any signature of call target.

Th error message gives us a clue on how to solve this problem. It says that the supplied parameters (1 and 3) do not match any signature of call target. Well, lets make a signature that does match the called function! Even better, lets make two of them!

Signatures

Two signatures are relevant in case of the curried function add:

function add(x: number): (y: number) => number;
function add(x: number, y: number) => number;

The first signature checks if add is called with one parameter (argument), in which case it returns a function that takes one argument and returns a number. If add is called with two arguments, the second signature applies and a number (the sum of the two supplied arguments) must be returned.

These two signatures are implemented like:

function add(x: number, y?: number): any {
    if (y === undefined) {
       return function(y: number) {
           return x + y;
       }
    } else {
       return x + y;
    }
}

When we compile this function and its two (overload) signatures it returns a function or a number depending on the supplied arguments of add. Note that the curried function has any as its return type. This is to make the compiler relax, which would otherwise have to choose between returning type (y: number) => number and number. By specifying any, the responsibility of setting the return type is left to the developer that defines the overload signatures.

General currying

Althought currying function add works fine, it is a lot of boilerplate to get it up and running. Lets try to generalish this concept of currying functions, but let us first limit to functions of two arguments and generalish from there.

Functional javascript library Ramda uses the following function that resembles our previous curried add function a lot. I added the type definitions.

function curry2(fn: Function): (a: number, b: number) => number {
    return function(a, b) {
        switch (arguments.length) {
            case 0:
                new TypeError('Function called with no arguments');
            case 1:
                return function(b: number) {
                    return fn(a, b);
                };
            default:
                return fn(a, b);
        }
    };
}

When we curry add using curry2 and run the same set of examples we get:

function add(x: number, y: number): number {
    return x + y;
}

var curriedAdd = curry2(add);
var res1: Function = curriedAdd(1);                 // compile error
var res2: (y: number) => number = curriedAdd(1);    // compile error
var res3: number = curriedAdd(1)(3);                // compile error
var res4: number = curriedAdd(1, 3);                // no error!!

The three compile error are all the same, the first one being:

curry2.ts(69,26): error TS2346: Supplied parameters do not match any signature of call target.

(69,26) indicates the start of curriedAdd(1). Like in the earliers error messages, TypeScript has a hard time determining what signature for curriesAdd is should us. We should help it with that by specifying the possible signatures as function overloads. But there is one complication. We are not talking about signature of curry2 but signature of the function returned by curry2: the curried add. How do we specify the signature of a function returned by another function? We can not because it is not one signature but in our case it are two.

I have not found a perfect solution to this problem. The best I have come sofar is to provide the signature and a fake function implementation. This fake function is later referenced to the curried function:

function curriedAdd(x: number): (y: number) => number;  // two overload signatures
function curriedAdd(x: number, y: number): number;      // ...
function curriedAdd(x: number, y?: number): any {};     // fake implementation

curriedAdd = curry2(add);                               // reference to curried add

The previous compile error are gone, but a new, less sever one is returned:

curry2.ts(100,5): error TS2364: Invalid left-hand side of assignment expression.

The javascript that is generated by TypeScript is correct and TypeScript is capable to check if the types of res1 to res4 are matching. The new error is ugly however.

Generic curry function

The library Ramda we used for our currying function is written in javascript and not in TypeScript. We need to provide a separate type definition file to use Ramda without the need to add type definition in the functions themselves. This is done in the external d.ts file.

In case of curry2 the separate type definition would be:

interface RamdaStatic {
    curry2(fn: (a: T1, b: T2) => T3): Function;
}

Side-note: curry2 is a private function in Ramda. It is used in this example as an illustration only.

@paldepind
Copy link

Please correct me if I'm wrong, but none of the typings for curry in this thread works at all with functions that have generics. This means that the curry function only works with the simplest of functions.

function map<A>(fn: (a: A) => B, array: A[]): B[] {
  // ...
}

const curriedMap = curry2(map); // does not have proper type

@iha2
Copy link

iha2 commented Mar 3, 2018

@paldepind your right non of this works with any functions with generics. When working with functions with generics type any is returned. It seems the Typescript static checker gives up. See typed-typings/npm-ramda#78

@neoneo
Copy link

neoneo commented Mar 13, 2018

@paldepind @iha2 One interesting approach I found is to introduce an additional interface like this:

interface Map {
    <A, B>(fn: (a: A) => B, array: A[]): B[]
    <A, B>(fn: (a: A)) => B): (array: A[]) => B[]
}

and then cast to it after currying:

const curriedMap = curry2(map) as Map;

You end up with a lot of duplicated type signatures, but if it's part of a library that could be acceptable.

@wagerfield
Copy link

@donnut could you please explain what the difference is between:

// @albrow's original solution
interface CurriedFunction2<T1, T2, R> {
  (t1: T1): <T2>(t2: T2) => R
  (t1: T1, t2: T2): R
}

// your "compact" solution
interface CurriedFunction2<T1, T2, R> {
  <T1>(t1: T1): <T2>(t2: T2) => R
  <T1,T2>(t1: T1, t2: T2): R
}

When I try and add the <T1, T2> generics to front of functions in the interface I get a warning from TSLint saying "Shadowed name: 'A' (no-shadowed-variable)". This is a linting error that can obviously be suppressed, but I would like to know what this adds to the solution.

Many thanks in advance!

@gtkatakura-bysoft
Copy link

gtkatakura-bysoft commented Sep 21, 2018

@donnut @albrow

I came in a more "generic" solution (with a little problem with undefineds) in TypeScript 3.1:

type Tail<F extends Function, S extends Number> =
  S extends 0 ? (F extends (...args: infer TArgs) => any ? TArgs : never) :
  S extends 1 ? (F extends (a: any, ...args: infer TArgs) => any ? TArgs : never) :
  S extends 2 ? (F extends (a: any, b: any, ...args: infer TArgs) => any ? TArgs : never) :
  S extends 3 ? (F extends (a: any, b: any, c: any, ...args: infer TArgs) => any ? TArgs : never) :
  S extends 4 ? (F extends (a: any, b: any, c: any, d: any, ...args: infer TArgs) => any ? TArgs : never) :
  S extends 5 ? (F extends (a: any, b: any, c: any, d: any, e: any, ...args: infer TArgs) => any ? TArgs : never) :
  S extends 6 ? (F extends (a: any, b: any, c: any, d: any, e: any, f: any, ...args: infer TArgs) => any ? TArgs : never) :
  never

type TailArray<A extends any[], S extends Number> =
  Tail<(...args: A) => any, S>

type Args<T extends Function> =
  T extends (...args: infer TArgs) => any ? TArgs
  : never

type PartialArgs<T extends Function> =
  T extends (...args: infer TArgs) => any ? Partial<TArgs>
  : never

type Curried<T extends (...args: any) => any, TReturn = ReturnType<T>> =
  <
    TArgs extends PartialArgs<T>,
    TRest extends TailArray<Args<T>, TArgs['length']>
  >(...args: TArgs) =>
    TRest extends []
      ? TReturn
      : Curried<(...args: TRest) => TReturn>

type Curry = <TFunc extends (...args: any) => any>(func: TFunc) => Curried<TFunc>

declare const curry: Curry

const curried = curry((a: 1 | undefined, b: number | number[], c: string) => 1)

// works :D
const a = curried(1)([2])('x')
const b = curried(1)(2, 'x')
const c = curried(1, 2)('x')
const d = curried(1, 2, 'x')


// the only problem is that `undefined` is accepted
// Partial<[1, 2]> => [1 | undefined, 2 | undefined]
curried(undefined)(2)(undefined)

@gtkatakura
Copy link

gtkatakura commented Sep 21, 2018

I got it, real variadic curry (only TS 3.1):

type Args<T extends Function> =
  T extends (...args: infer TArgs) => any ? TArgs
  : never

type TailArgs<F extends Function> =
  F extends (head: any, ...tail: infer TTail) => any ? TTail :
  never

type TailArray<A extends any[]> = TailArgs<(...args: A) => any>

type CastArray<T> = T extends any[] ? T : []

type DropFromArraySize<T extends any[], TSize extends any[]> = CastArray<{
  "end": T,
  "continue": DropFromArraySize<TailArray<T>, TailArray<TSize>>,
}[TSize extends [] ? "end" : "continue"]>

type PartialArgs<T extends Function> =
  T extends (...args: infer TArgs) => any ? Partial<TArgs>
  : never

type Curried<T extends (...args: any) => any, TReturn = ReturnType<T>> =
  <
    TInitArgs extends PartialArgs<T>,
    TTailArgs extends DropFromArraySize<Args<T>, TInitArgs>
  >(...args: TInitArgs) =>
    TTailArgs extends []
      ? TReturn
      : Curried<(...args: TTailArgs) => TReturn>

type Curry = <TFunc extends (...args: any) => any>(func: TFunc) => Curried<TFunc>

declare const curry: Curry

const curried = curry((a: 1 | undefined, b: number | number[], c: string) => 1)

// works :D
const a = curried(1)([2])('x')
const b = curried(1)(2, 'x')
const c = curried(1, 2)('x')
const d = curried(1, 2, 'x')

// the only problem is that `undefined` is accepted
// Partial<[1, 2]> => [1 | undefined, 2 | undefined]
curried(undefined)(2)(undefined)
curried(undefined)(2)('2')

@gtkatakura
Copy link

gtkatakura commented Sep 21, 2018

I solved the problem of undefined, here:

The changes:

// new types

type Init<T extends any[], TTail extends any[] = TailArray<T>> = CastArray<{
  [K in keyof TTail]: T[keyof T & K];
}>

type PotentialArgs<T extends any[], TResult extends any[] = T> = {
  "continue": PotentialArgs<Init<T>, TResult | Init<T>>;
  "end": TResult;
}[T extends [] ? "end" : "continue"]

// changes
type Curried<T extends (...args: any) => any> =
  <
    TInitArgs extends PotentialArgs<Args<T>>, // use here
    TTailArgs extends DropFromArraySize<Args<T>, TInitArgs>
  >(...args: TInitArgs) =>
    TTailArgs extends []
      ? ReturnType<T>
      : Curried<(...args: TTailArgs) => ReturnType<T>>

@joshburgess
Copy link

joshburgess commented Sep 24, 2018

@gtkatakura When I tried the above it crashed my editor, the TS compiler, and also the online TS playground.

@patrickmichalina
Copy link

@joshburgess be sure to use TS 3.1. It appears to work so far!

@millsp
Copy link

millsp commented Mar 1, 2019

Hi everyone,

I wrote a real solution for this problem. If you would like to find out how it's done:
https://medium.freecodecamp.org/typescript-curry-ramda-types-f747e99744ab

@enricopolanski
Copy link

@pirix-gh your implementation still can't infer correctly pretty simple cases:

`
import { F } from 'ts-toolbelt'
declare function curry(fns: Fns): F.Curry

const tuple = <A, B>(a: A, b: B): [A, B] => [a, b]

// const f: F.Curry<(a: any, b: any) => [any, any]>
const f = curry(tuple)

// x: [any, any]
const x = f(1)('a')
`

@millsp
Copy link

millsp commented Jul 25, 2019

@VulcanRav, yes it is a limitation that is related to typescript.
Being discussed here microsoft/TypeScript#5453 (comment)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment