Currying in TypeScript

Jamie Pennell
CodeX
Published in
9 min readMay 17, 2021
Curry photo by Andy Hey @eastcoastkitchen

What is Currying?

Currying is a coding technique of converting a function with a set number of parameters in to a chain-able function, allowing it to have its parameters provided in stages.

For example, currying fn(1,2,3) would allow it to be called like fn(1)(2)(3) or fn(1)(2,3) etc.

Heres an example of a currying function:

function curry(targetFn, …existingArgs) {
return function(…args) {
const totalArgs = […existingArgs, …args]
if(totalArgs.length >= targetFn.length) {
return targetFn(…totalArgs)
}
return curry(targetFn, …totalArgs)
}
}

Note the use of the length property of the target function. Because of this, currying can only work on functions with required parameters. Thats not to say that the last call of a curried function cannot provide the optional parameters of course.

There are many uses for this, such as logger functions, or builders. But one issue with them, at least with JavaScript, is that the resulting curried function can lose its parameter information. As seen below:

Plain JS autocomplete

Luckily, this is something TypeScript can help us with. But it’s going to take some fairly complex types to achieve.

The Scenario

For our curried function, we have 2 separate moving parts we need to take account of when determining its specification:

  • The original function — this can have any parameters and any return type, but we need to be aware of them
  • The previously provided parameters — these can be any amount of the original functions parameters, but only in the order they were specified.

We can use the above to determine the type of the parameters for the curried function, namely any amount of the original functions remaining parameters

We can then use all of the above to determine the parameters and return type of the curried function.

  • The parameters will be the remaining required parameters of the original function
  • If the previously provided parameters + the latest parameters are greater than or equal to the length of the original functions parameters, the return type will be that of the original functions return type.
  • Else the return type is another curried function using the original function and all the currently provided parameters

So, we have 3 main types we need to define.

The PartialParameters type

The first type we need is one that given a function, returns a tuple representing the function parameters if all the parameters were considered optional.

This should have been an easy type for us to create, because we can almost build it from the existing utility types that TypeScript provides.

  • Partial — this type takes all keys in a provided type and makes them optional.
  • Parameters — this type extracts a tuple representing the parameters of a given function.

The end result would have looked like this.

type PartialParameters<
FN extends (…args: any[]) => any
> = Partial<Parameters<FN>>;

However, in some use cases, TypeScript will sometimes not see the result of Partial<any[]> to be an array type, and as such we cant use it with rest parameters. The issue is talked about here https://github.com/microsoft/TypeScript/issues/29919.

function foo<
F extends (…args: any[]) => any
>(fn: F, …args: Partial<Parameters<F>>) { }
// ERROR: A rest parameter must be of an array type.ts(2370)

So, we need to repeat this functionality in a way that retains the output of the type as a tuple.

To do that we will use a utility type, whose job it will be to iterate through a given tuple to find all the combinations, and a wrapper type that feeds a functions parameters to this utility type.

Heres our utility type, it uses recursion and inference to retrieve all required items in a tuple, one by one, and adds them to the output type as optional values, before finally adding any existing optional values to the output type.

type PartialTuple<
TUPLE extends any[],
EXTRACTED extends any[] = []
> =
// If the tuple provided has at least one required value
TUPLE extends [infer NEXT_PARAM, ...infer REMAINING] ?
// recurse back in to this type with one less item
// in the original tuple, and the latest extracted value
// added to the extracted list as optional
PartialTuple<REMAINING, [...EXTRACTED, NEXT_PARAM?]> :
// else if there are no more values,
// return an empty tuple so that too is a valid option
[...EXTRACTED, ...TUPLE]

Inference and recursion are going to be our best friends during this exercise. They allow us to look at each part of the provided tuple one by one until we’ve gone through every required parameter.

Now our PartialParameters type can use this utility type instead of partial, with the same result, but no confusion from TypeScript about whether or not it is an array type.

type PartialParameters<FN extends (…args: any[]) => any> =
PartialTuple<Parameters<FN>>;
// EXAMPLEtype PartialParametersExample = PartialParameters<
(a: boolean, b: string, c?: number) => any
>
// type PartialParametersExample = [boolean?, string?, number?]

The RemainingParameters Type

As mentioned previously, one of the other types we need to be able to account for is the subset of required parameters that the user still needs to provide. So, if the user of our curried function has provided 3 arguments, and there are 5 required parameters in total, the type should return a tuple containing the remaining 2 parameters types.

Unfortunately we can’t just do Array.slice, and we can’t use the length of the provided parameters to extract a given amount of the expected parameters, but we can analyse the length of the tuple types with recursion. Here’s how:

type RemainingParameters<
PROVIDED extends any[],
EXPECTED extends any[]
> =
// if the expected array has any required items…
EXPECTED extends [infer E1, …infer EX] ?
// if the provided array has at least one required item,
// recurse with one item less in each array type
PROVIDED extends [infer P1, …infer PX] ?
RemainingParameters<PX, EX> :
// else the remaining args is unchanged
EXPECTED :
// else there are no more arguments
[]

Essentially, what this is doing is removing required items from the front of both tuple types until one of them no longer has any required items remaining. If its the EXPECTED tuple thats empty, return an empty tuple (not the expected tuple, as it will still contain optional values, and we would never get to the point of having an empty tuple). If it’s the PROVIDED tuple, then return whats left in the EXPECTED tuple.

Again, here are some examples:

type RemainingSimple = RemainingParameters<
[1, 2],
[number, number, number]
>
// type RemainingSimple = [number]
type RemainingOverflow = RemainingParameters<
[1, 2, 3, 4],
[number]
>
//type RemainingOverflow = []

The eagle eyed amongst you may have noticed that this doesn’t actually assert the types of the items are equal, so a tuple of strings could be compared against a tuple of numbers and this type wouldn’t care. Fortunately we don’t need it to, because by the time we’re using this type, the other parts of our curried functions typing would have asserted the type anyway.

But, if we did want to asset that the typings were correct, we could do something like this.

type RemainingParameters<
PROVIDED extends any[],
EXPECTED extends any[]
> =
// if the expected array has any required items…
EXPECTED extends [infer E1, …infer EX] ?
// if the provided array has at least one required item
PROVIDED extends [infer P1, …infer PX] ?
// if the type is correct, recurse with one item less
//in each array type
P1 extends E1 ? RemainingParameters<PX, EX> :
// else return this as invalid
never :
// else the remaining args is unchanged
EXPECTED :
// else there are no more arguments
[]

If we did it like this, then for each level of recursion, if the provided parameter was not a valid extension of the expected type, the type never would be returned.

// without the type validation
type RemainingDifferentTypes = RemainingParameters<
[1],
[string, string]
>
// type RemainingDifferentTypes = [string]
// with the type validation
type RemainingDifferentTypes = RemainingParameters<
[1],
[string, string]
>
// type RemainingDifferentTypes = never

The CurriedFunction Type

Okay, this is the last type we have to make… or perhaps I should say last two types, because we’re going to need to separate the logic out. We’ll need one type for the actual function definition, and another type to define its return value.

Lets start with the main functions type:

type CurriedFunction<
PROVIDED extends any[],
FN extends (…args: any[]) => any
> =
<NEW_ARGS extends PartialTuple<
RemainingParameters<PROVIDED, Parameters<FN>>
>>(…args: NEW_ARGS) =>
CurriedFunctionOrReturnValue<[…PROVIDED, …NEW_ARGS], FN>

This type has two generic arguments, one representing all the arguments that have been provided previously, and one representing the type of the curried function.

We give the returned function type a generic argument here too, built up from the previous generic types we provided, and the types we built earlier. Combining PartialTuple and RemainingParameters with the previous arguments and the functions parameters allows us to specify the return types parameters to be “any amount of the remaining function parameters”.

Capturing the type of the parameters using NEW_ARGS extends allows TypeScript to see exactly which parameters are provided at the time the function is called, rather than just knowing it to be any of the valid options. Because we know exactly what they are, we can combine them with the previous parameters and pass the combined tuple type in to the CurriedFunctionOrReturnValue type, which we will build next.

type CurriedFunctionOrReturnValue<
PROVIDED extends any[],
FN extends (…args: any[]) => any
> =
RemainingParameters<PROVIDED, Parameters<FN>>
extends [any, …any[]] ?
CurriedFunction<PROVIDED, FN> :
ReturnType<FN>

This one isn’t too complicated, it uses the provided parameters generic type to compare against the expected parameters of the curried function. If it finds there to be any arguments remaining, the return type becomes another CurriedFunction, this time with the latest provided parameters. If all the parameters have been specified however, its return type becomes that of the original curried function.

Putting it All Together…

Now lets apply these types to our curry function:

function curry<
FN extends (…args: any[]) => any,
STARTING_ARGS extends PartialParameters<FN>
>(targetFn: FN, …existingArgs: STARTING_ARGS):
CurriedFunction<STARTING_ARGS, FN>
{
return function(…args) {
const totalArgs = [...existingArgs, ...args]
if(totalArgs.length >= targetFn.length) {
return targetFn(...totalArgs)
}
return curry(targetFn, ...totalArgs as PartialParameters<FN>)
}
}

We've give it 2 generic types, the first of which will be the type of the function supplied in the first parameter, the second will be the type of the parameters already supplied.

As with the generic type NEW_ARGS above, we specify both of these as generic types so we can capture a finer level of detail about them.

If we simply put (targetFn: (...args: any[]) => any, ...existingArgs: PartialParameters<FN>) then we would have to use those types in their most generic format. We wouldn't be able to tell the type of the parameters or the return type of the function, nor exactly which arguments have been provided upfront.

By specifying the return type of our function as a CurriedFunction, we also offer information to the compiler about the return type and parameters of the inner function, which saves us having to repeat ourselves.

The only issue with the typings are that we have to cast totalArgs to PartialParameters<FN>. We know this is right, but the compiler doesn't. We could potentially build another type to help the compiler out here. But it just wouldn't be worth the effort.

Now all thats left is to try it out:

function buildString(a: number, b: string, c: boolean): string {
return `The ${a.toString(36)} ${b} ${c ? "truth" : "lie"}!`
}
const curried = curry(buildString)
// const curried: CurriedFunction<
// [],
// (a: number, b: string, c: boolean) => string
// >
const invalid = curried("not a number")
// Argument of type 'string' is not assignable
// to parameter of type 'number'.ts(2345)
const invalid2 = curried(123)({not: "a string"})
// Argument of type '{ not: string; }' is not assignable
// to parameter of type 'string'.ts(2345)
const partway = curried(573566, "is a")
const result = partway(false)
// const result: string
console.log(result)
// Yes, I really did make that outdated a reference

So, thats it. Thanks for reading! Hopefully you found this helpful. There can sometimes be a lot of hoops that you have to jump through to get something to work nicely with TypeScript, but the ability to add validation where before there was none can be very useful.

Anything you would have done different? Leave a comment! This is the first time I've ever written a guide like this so feedback will always be welcome.

--

--

CodeX
CodeX

Published in CodeX

Everything connected with Tech & Code. Follow to join our 1M+ monthly readers

Jamie Pennell
Jamie Pennell

Written by Jamie Pennell

I'm a full-stack developer with a focus towards the front end and a keen interest in React and TypeScript.

Responses (2)