#17Hard

Currying 1

Type a currying function that turns a multi-argument function into a chain of single-argument calls. Recursive tuple inference peels one parameter per step.

The recursion in this one is the easy part; the constraint on the generic is where the tests bite.

Currying converts a function that takes multiple arguments into a sequence of functions that each take a single argument. Your Currying type must make the curried result accept exactly one argument at a time and yield the original result once all arguments are assigned. The passed-in function may have any number of parameters, and the type has to peel them off one by one without losing a parameter type or the return type. (TypeScript 4.0 or later is recommended, since the solution leans on variadic tuple types.)

const add = (a: number, b: number) => a + b
const three = add(1, 2)
 
const curriedAdd = Currying(add)
const five = curriedAdd(2)(3)

Challenge Instructions: Currying 1

Hard

TypeScript 4.0 is recommended in this challenge

Currying is the technique of converting a function that takes multiple arguments into a sequence of functions that each take a single argument.

For example:

const add = (a: number, b: number) => a + b
const three = add(1, 2)
 
const curriedAdd = Currying(add)
const five = curriedAdd(2)(3)

The function passed to Currying may have multiple arguments, you need to correctly type it.

In this challenge, the curried function only accept one argument at a time. Once all the argument is assigned, it should return its result.

View on GitHub: https://tsch.js.org/17

Change the following code to make the test cases pass (no type check errors).

Loading...

Detailed Explanation

The finished solution:

type Curried<F> = F extends (...args: infer Args) => infer Return
  ? Args extends [infer First, ...infer Rest]
    ? Rest extends []
      ? (arg: First) => Return
      : (arg: First) => Curried<(...args: Rest) => Return>
    : () => Return
  : never
 
declare function Currying<F extends Function>(fn: F): Curried<F>

The core idea: capture the parameter list as a tuple, then peel one parameter off the front per step, wrapping what remains in a new function type each time.

Capturing the parameters as a tuple

F extends (...args: infer Args) => infer Return extracts two things from any function type: Args, the parameter list as a tuple type, and Return. For (a: string, b: number, c: boolean) => true you get:

// Args   = [a: string, b: number, c: boolean]
// Return = true

Getting parameters as a tuple is what makes the rest possible. Tuples can be destructured with variadic patterns; function signatures themselves cannot.

Peeling one argument at a time

Args extends [infer First, ...infer Rest] is the tuple analogue of a head/tail split in functional programming. First grabs the first element's type; the spread ...infer Rest captures everything after it as a new tuple. Then the recursion kicks in:

[object Object]

We emit one single-argument function and delegate the remaining parameters by rebuilding a smaller function type (...args: Rest) => Return, then running Curried on it again. Tracing curried1 from the tests:

Curried<(a: string, b: number, c: boolean) => true>
// = (arg: string) => Curried<(b: number, c: boolean) => true>
// = (arg: string) => (arg: number) => Curried<(c: boolean) => true>
// = (arg: string) => (arg: number) => (arg: boolean) => true

Each recursion step consumes exactly one parameter, so a seven-parameter function (like curried2 in the tests) unrolls into a chain of seven single-argument functions. No manual overloads required.

The two base cases

The recursion bottoms out in two different ways, and both matter:

Why F extends Function

You might instinctively constrain the generic as F extends (...args: any[]) => any and then watch the tests fail. The reason is contextual typing: with a call-signature-shaped constraint, the lambda you pass in gets contextually typed against (...args: any[]) => any, and its literal return type true widens to boolean during inference. The test Equal<typeof curried3, () => true> then fails, because () => boolean is not exactly () => true.

Constraining with Function instead (or leaving F unconstrained) gives TypeScript no call signature to contextually type against, so the lambda's own inferred type, literal true and all, flows into F untouched. It is a rare case where the looser constraint is the correct one.

Edge cases the tests cover

Infer the parameter tuple, then split and recurse on a rebuilt function type: that pattern is the backbone of most type-level list processing in TypeScript.

This challenge is originally from here.

Share this challenge

Learn the Concepts