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
?
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);
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.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With