Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Type system for function composition

How would I add types for compose?
The problem basically boils down to writing the types for this:

const compose = (...funcs) => x => funcs.reduce((acc, func) => func(acc), x);

and using it:

compose(x => x + 1, x => x * 2)(3);

In this example, the type of compose is inferred to:

const compose: (...funcs: any[]) => (x: any) => any

Which is just a bunch of any...

Is there a good way to add types to compose?

like image 737
Ben Avatar asked Mar 28 '18 07:03

Ben


2 Answers

While it is not possible to type such a function to accept any number of functions, we can write a version of compose that has overloads of up to a given number of functions. For most practical uses it should be enough allow composition of up to 5 functions, and we can always add more overloads as the need arises.

I also added an overload for when the types are not different and we only have one type that is being processed throughout compose. This allows an arbitrary number of functions to be passed in if the parameter type and return type are the same.

function compose<A, B, C, D, E, F>(fn: (p: A) => B, fn2: (p: B) => C, fn3: (p: C) => D, fn4: (p: D) => E, fn5: (p: E) => F): (p: A) => F
function compose<A, B, C, D, E>(fn: (p: A) => B, fn2: (p: B) => C, fn3: (p: C) => D, fn4: (p: D) => E): (p: A) => E
function compose<A, B, C, D>(fn: (p: A) => B, fn2: (p: B) => C, fn3: (p: C) => D): (p: A) => D
function compose<A, B, C>(fn: (p: A) => B, fn2: (p: B) => C): (p: A) => C
function compose<T>(...funcs: Array<(p: T) => T>) : (p: T) => T // Special case of parameter and return having the same type
function compose(...funcs: Array<(p: any) => any>) {
    return (x: any) => funcs.reduce((acc, func) => func(acc), x)
};

// Usage
// First argument type must be specified, rest are inferred correctly
let fn = compose((x : number) => x + 1, x => x * 2); // fn will be (p: number) => number
let d = fn(3); // d will be number 

let fn2 = compose((x : number) => x + 1, x => x * 2, x=> x.toString(), x => x.toLowerCase()); // fn will be (p: number) => string and we get intelisense and type safety in each function 

// Any number of functions but a single type
let fnMany = compose((x : number) => x + 1, x => x * 2, x => x * 2, x => x * 2, x => x * 2, x => x * 2, x => x * 2); 
like image 184
Titian Cernicova-Dragomir Avatar answered Oct 22 '22 12:10

Titian Cernicova-Dragomir


You could introduce some type information, I typically recommend an iterative approach to this, for example this early version isn't very flexible - but results in the final type being correct:

interface Composer {
    (...funcs: { (arg: any): any }[]): (arg: any) => number;
}

const compose: Composer = (...funcs) => x => funcs.reduce((acc, func) => func(acc), x);

// result: number
const result = compose(x => x + 1, x => x * 2)(3);

You can iterate this towards your particular use. For example, it may be that the final type needs to be a generic type, so you could upgrade Composer to Composer<T>. Or you may want to make the intermediate types less permissive (they can currently be any type, but you can improve that too).

Ultimately, it is up to you to decide where to fix types, or introduce dynamic types, or to stop caring about types entirely within the Composer type.

like image 2
Fenton Avatar answered Oct 22 '22 10:10

Fenton