Typescript compose function

How I managed to get typescript to type check functional programming compose function?

Photo by Mike Kenneally on Unsplash

In functional programming the term composing functions is pretty common. Lets see how to do that first by native javascript then I am going to add types step by step to check and validate it.

Step 1:
Let’s say we have 3function getName, getLength and isEven these are just 3 javascript functions written as follows:

const getName = (p) => p.name;const getLength = (str) => str.length;const isEven = (num) => num % 2 === 0;

This is how we can use them:

const person = {
name: 'John',
};
const name = getName(person); // John
const length = getLength(name); // 4
const isEvenLength = isEven(length); // true

Step 2:
Let’s compose these functions together:

const myComposedFn = isEven(getLength(getName(person))); // true

That does not look nice, we have lots of parentheses and a bit difficult to read as well.

Can we compose these functions in a nice way? Yes..

Lets first write compose function in a generic way.

const compose = (...fns) => (x) => fns.reduceRight((acc, cur) => cur(acc), x);

This is a higher order function that accepts array of functions and return a function that accept an object and if called it returns the result of running these functions (from right to left) on that object.

const myComposedFn = compose(isEven, getLength, getName);
const result = myComposedFn(person); // true

Step 3:
Let’s rewrite the above 3 functions in typescript to get better type checks.

interface IPerson {
name: string;
}
const person: IPerson = {
name: "Mina",
};
const getName = (p: IPerson) => p.name;const getLength = (str: string) => str.length;const isEven = (num: number) => num % 2 === 0;

Step 4:
Let’s write the generic compose function in typescript to get better type checks.

Since a composable function is of arity 1 (arity means accept 1 parameter), then I can write a generic type for these functions as follows:

type ArityOneFn = (arg: any) => any;

Now I can write the type of the compose function rest argument as follows:

const compose = (...fns: ArityOneFn[]) => (p: any) => 
fns.reduceRight((acc: any, cur: any) => cur(acc), p);
const myComposedFn = compose(isEven, getLength, getName);
// In vscode if you hover on that function you would see:
// const myComposedFn: (p: any) => any

Sounds good?
Yes, but we are using any for both the input and output of myComposedFn
Can we change this?
- Yes.
How?

Step 5:
I need some helper types to represent the following:

  • The parameter type of this function getName in order to use it as a type for myComposedFn parameter. We can use built-in Parameters .
type ArityOneFn = (arg: any) => any;type PickLastInTuple<T extends any[]> = T extends [...rest: infer U, argn: infer L ] ? L : never;type FirstFnParameterType<T extends any[]> = Parameters<PickLastInTuple<T>>[any];type LastFnReturnType<T extends any[]> = ReturnType<T[0]>;

Now we can write the typescript version of the compose function:

const compose = <T extends ArityOneFn[]>(...fns: T) => (p: FirstFnParameterType<T>): LastFnReturnType<T> =>  fns.reduceRight((acc: any, cur: any) => cur(acc), p);

Done :)

Here is the full version of the code:

interface IPerson {
name: string;
}
const person: IPerson = {
name: "John",
};
const getName = (p: IPerson) => p.name;
const getLength = (str: string) => str.length;
const isEven = (num: number) => num % 2 === 0;
type ArityOneFn = (arg: any) => any;
type PickLastInTuple<T extends any[]> = T extends [...rest: infer U, argn: infer L ] ? L : never;
type FirstFnParameterType<T extends any[]> = Parameters<PickLastInTuple<T>>[any];
type LastFnReturnType<T extends any[]> = ReturnType<T[0]>;
const compose = <T extends ArityOneFn[]>(...fns: T) => (p: FirstFnParameterType<T>): LastFnReturnType<T> => fns.reduceRight((acc: any, cur: any) => cur(acc), p);const myComposedFn = compose(isEven, getLength, getName);
// In vscode if you hover on that function you would see:
// const myComposedFn: (p: IPerson) => boolean

Final note:
It took me about 15 hours to write the above types for the compose function, at that time I tried different things and read some articles, I thought it is not possible to get types of first and last argument of a tuple, I found some other implementations online but all with some limitations. But finally I got my version of a fully typed compose function :)
Cheeeeers