How to curry functions in TypeScript so that it always works correctly?

Adam Zieliński Avatar

Posted by

on

I recently tried to remove the first argument from the function signature:

type TwoArgsFunction = (state: {}, arg1: number) => string;

type OneArgFunction = RemoveFirstArg<TwoArgsFunction>;

type RemoveFirstArg = /* What goes here? */

Turns out it is nearly impossible! As of TypeScript 4.7, I only found one solution that works in all cases: Manually specifying both the initial and the transformed signature:

type WithCustomCurrySignature = {
	CurriedSignature: Function;
}

type RemoveFirstArg< F > =
	F extends WithCustomCurrySignature
		? F['CurriedSignature']
		: F extends ( state: any, ...args: infer P ) => infer R
			? ( ...args: P ) => R
			: F;


interface TwoArgsFunction extends WithCustomCurrySignature {
	(state: {}, myNumberArg: number): string;
	CurriedSignature: (myNumberArg: number) => string;
}

type OneArgFunction = RemoveFirstArg<TwoArgsFunction>;

The simple solution does not preserve generic types

I noticed this currying problem while working on adding the TypeScript support to WordPress. The Redux selectors are curried:

// In selectors.ts, the state is one of the arguments
export function getEntityConfig( state, kind, name ) { /* */ }

// The consumer code doesn't pass the state, though
select( coreStore ).getEntityConfig( kind, name );

The select function needs to return a version of the getEntityConfig that doesn’t expect the state argument.

A simple solution would be to handle it as follows:

type RemoveFirstArg< F > = F extends ( state: any, ...args: infer P ) => infer R
	? ( ...args: P ) => R
	: F;

It works well with simple function signatures:

type TwoArgFunction = (state: {}, arg1: number) => string;
type OneArgFunction = RemoveFirstArg< TwoArgFunction >;
// OneArgFunction is (myNumberArg: number) => string;

Unfortunately, it doesn’t correctly curry functions using generics:

type Selector = <K extends string | number>(
    state: any,
    arg1: K,
    arg1Type: K extends string ? 'string' : 'number'
) => K;

type IncorrectlyCurriedSignature = RemoveFirstArg< Selector >
// IncorrectlyCurriedSignature evaluates to:
// (arg1: string | number, arg1Type: "string" | "number") => string | number

A universal RemoveFirstArg type may not exist in TypeScript

I don’t believe TypeScript supports the kind of automatic transformation the RemoveFirstArg wants to do. I’ve spent hours trying to build one, and my every attempt failed.

Parameters type cannot be extracted from a generic function signature

The F extends ( state: any, ...args: infer Params ) => Return feature does not correctly infer Params nor Return.

The Parameters helper does not help either:

Parameters< Selector >
// [arg1: string | number, arg1Type: "string" | "number"]

Instantiation expressions can’t express the generic signature either

TypeScript has a new feature called “instantiation expressions” that allows you to say typeof selector<'root'>:

function selector<K extends string | number>(
    state: any,
    arg1: K,
    arg1Type: K extends string ? 'string' : 'number'
): K { return {} as K; };

type TypeOfSignature = typeof selector<'foo'>;
// (state: any, arg1: "foo", arg1Type: 'string') => 'foo';

The inferred signature reflects the constraints of the selector definition, which is great. The problem now is that it’s only for the specific K equal to root. We want to make it work for all K.

Unfortunately, passing a type union as K moves us back to square one:

type InstantiatedTypeOfSignature = typeof selector<'foo' | 10>;
// (state: any, arg1: "foo" | 10 , arg1Type: "string" | "number") => "foo" | 10

Parametrized clone doesn’t cut it either

I’ve also tried my luck with Parametrization

There aren’t many ways of formulating such a parametrized clone type I can think of. I only found the following Curry implementation on StackOverflow:

type FN1<A, R>             = (a: A) => R
type FN2<A, B, R>          = ((a: A, b: B) => R)             | ((a: A) => FN1<B, R>)
type FN3<A, B, C, R>       = ((a: A, b: B, c: C) => R)       | ((a: A, b: B) => FN1<C, R>)       | ((a: A) => FN2<B, C, R>)

interface RemoveFirstArg {
    <A, R>(fn: (arg0:A) => R): FN1<A, R>
    <A, B, R>(fn: (arg0:A, arg1:B) => R): FN2<A, B, R>
    <A, B, C, R>(fn: (arg0:A, arg1:B, arg2:C) => R): FN3<A, B, C, R>
}

const removeFirstArg = {} as RemoveFirstArg;
removeFirstArg(selector);

Unfortunately, this causes an “insufficient constraints” error:

Types of parameters 'arg1' and 'arg1' are incompatible.
    Type 'unknown' is not assignable to type 'string | number'

I didn’t find any way of specifying the generic type constraints that would make it work.

You can’t even slice a parametrized tuple

Once the type constraints are baked into a function type signature, they are stuck there. I haven’t found any TypeScript feature that could split the function apart into a constrained arguments type and a constrained return type.

But what would happen if we defined the arguments tuple manually?

type SelectorArgs<K extends string | number> = [
    state: any,
    arg1: K,
    arg1Type: K extends string ? 'string' : 'number'
]

So far so good, now let’s try to remove its first element:

type SlicedArgs<K extends string | number> = SelectorArgs<K> extends [any, infer A1, infer A2] ? [A1, A2] : never;
const selectorWithSlicedArgs = <K extends string | number>(...args:SlicedArgs<K>) => { return {} as K };

selectorWithSlicedArgs( 'foo', 'string' ) // ✅ 'foo'
selectorWithSlicedArgs( 'foo', false )    // ✅ Type error
selectorWithSlicedArgs( 10, 'number' )    // ✅ 10
selectorWithSlicedArgs( 10, false )       // ✅ Type error

There is hope, TypeScript respects the original constraints. Let’s try a more complex SelectorArgs type, then:

type Character = { first: 'harry', last: 'potter' } | { first: 'severus', last: 'snape' };
type GetFirstName<FirstName extends string> = [
    state: any,
    first: FirstName,
    last: Extract<Character, { first: FirstName }>['last']
]
type SlicedArgs<K extends string | number> = GetFirstName<K> extends [any, infer A1, infer A2] ? [A1, A2] : never;
const getName = <K extends string | number>(...args:SlicedArgs<K>) => null;

getFirstName( 'harry', 'potter' ) // ✅ 'harry'
getFirstName( 'harry', false )    // ✅ Type error
getFirstName( 'harry', 'snape' )  // ❌ 'harry'
getFirstName( 'foo',   'snape' )  // ❌ 'foo'

Bummer! Adding Extract to the mix breaks the inference and yields a selectorWithSlicedArgs that simply accepts 'one value' | false as its second argument.

Interestingly, we still can provide the constraint manually:

getFirstName<'harry'>( 'harry', 'potter' ) // ✅ 'harry'
getFirstName<'harry'>( 'harry', 'snape' )  // ✅ Type Error

It’s cool, but it doesn’t help much.

This leaves us with only one option.

Providing the curried and uncurried type signatures manually is the only reliable option

I want to do two things with the selector function

Let’s define a type that is both callable and also contains information about the manually-defined curried signature:

interface WithCustomCurrySignature {
    CurriedSignature: Function;
}
type Character = { first: 'harry', last: 'potter' } | { first: 'severus', last: 'snape' };
interface GetFirstName extends WithCustomCurrySignature {
    <FirstName extends string>(
        state: any,
        first: FirstName,
        last: Extract<Character, { first: FirstName }>['last']
    ): FirstName;

    CurriedSignature: <FirstName extends string>(
        first: FirstName,
        last: Extract<Character, { first: FirstName }>['last']
    ) => FirstName;
}

Here’s a proof it can be called just like a regular function:

const getFirstName = {} as GetFirstName;
getFirstName({}, 'harry', 'potter' ); // ✅ 'harry'
getFirstName({}, 'harry', 'snape' );  // ✅ Type error
getFirstName({}, 'harry', 10 );       // ✅ Type error

getFirstName({}, 'severus', 'snape' );  // ✅ 'severus'
getFirstName({}, 'severus', 'potter' ); // ✅ Type error
getFirstName({}, 'severus', 10 );       // ✅ Type error

It even respects our type constraints, great!

Now, let’s try currying:

type CurriedState< F > =
    F extends WithCustomCurrySignature
        ? F['CurriedSignature']
        :
    F extends ( state: any, ...args: infer P ) => infer R
        ? ( ...args: P ) => R
        : F;

const curried = {} as CurriedState<GetFirstName>;

curried('harry', 'potter' ); // ✅ 'harry'
curried('harry', 'snape' );  // ✅ Type error
curried('harry', 10 );       // ✅ Type error

curried('severus', 'snape' );  // ✅ 'severus'
curried('severus', 'potter' ); // ✅ Type error
curried('severus', 10 );       // ✅ Type error

Brilliant! And, in case you wondered, CurriedState can still automatically curry any regular function:

type NumberToString = (state: {}, myNumberArg: number) => string;
const numberToString = {} as NumberToString;
numberToString({}, 15);   // ✅ Eeturns a string
numberToString({}, '15'); // ✅ Type error

const curried = {} as CurriedState<NumberToString>;
curried(15);   // ✅ Returns a string
curried('15'); // ✅ Type error

There are downsides, of course. Relying on the CurriedSignature property means there could be a name conflict with another type. Also, it doesn’t make for the most readable code out there.

Still, it works. I never expected to arrive at such as complex solution to such a simple problem!

Leave a Reply

Blog at WordPress.com.

%d