In flow there is support for $Compose
functions (see recompose as example). However, I can't seem to find such a mechanism in typescript. it seems that the best typescript can do is something like https://github.com/reactjs/redux/blob/master/index.d.ts#L416-L460. What's the equivalent to $Compose
in Typescript?
EDIT: What I'm trying to accomplish is to type the compose
function from recompose
or redux
such that it's typesafe. In particular, with react higher order components, I want to ensure that the output props of one HOC satisfy the input props of the next HOC. This is my current workaround and seems to work reasonably well - though I was hoping there is a good way to do this natively in typescript.
/** Wraps recompose.compose in a type-safe way */
function composeHOCs<OProps, I1, IProps>(
f1: InferableComponentEnhancerWithProps<I1, OProps>,
f2: InferableComponentEnhancerWithProps<IProps, I1>,
): ComponentEnhancer<IProps, OProps>
function composeHOCs<OProps, I1, I2, IProps>(
f1: InferableComponentEnhancerWithProps<I1, OProps>,
f2: InferableComponentEnhancerWithProps<I2, I1>,
f3: InferableComponentEnhancerWithProps<IProps, I2>,
): ComponentEnhancer<IProps, OProps>
function composeHOCs<OProps, I1, I2, I3, IProps>(
f1: InferableComponentEnhancerWithProps<I1, OProps>,
f2: InferableComponentEnhancerWithProps<I2, I1>,
f3: InferableComponentEnhancerWithProps<I3, I2>,
f4: InferableComponentEnhancerWithProps<IProps, I3>,
): ComponentEnhancer<IProps, OProps>
function composeHOCs(
...fns: Array<InferableComponentEnhancerWithProps<any, any>>
): ComponentEnhancer<any, any> {
return compose(...fns)
}
I read your question as follows:
How can I give a TS type to this higher-order function, such that the type of
x
is allowed to vary across the loop?function compose(...funs) { return function(x) { for (var i = funs.length - 1; i >= 0; i--) { x = funs[i](x); } return x; } }
The bad news is that you can't type this function directly. The funs
array is the problem - to give compose
its most general type, funs
should be a type-aligned list of functions - the output of each function must match the input of the next. TypeScript’s arrays are homogeneously typed - each element of funs
must have exactly the same type - so you can't directly express the way the types vary throughout the list in TypeScript. (The above JS works at runtime because the types are erased and data is represented uniformly.) That's why Flow's $Compose
is a special built-in type.
One option to work around this is to do what you've done in your example: declare a bunch of overloads for compose
with varying numbers of parameters.
function compose<T1, T2, T3>(
f : (x : T2) => T3,
g : (x : T1) => T2
) : (x : T1) => T3
function compose<T1, T2, T3, T4>(
f : (x : T3) => T4,
g : (x : T2) => T3,
h : (x : T1) => T2
) : (x : T1) => T4
function compose<T1, T2, T3, T4, T5>(
f : (x : T4) => T5,
g : (x : T3) => T4,
h : (x : T2) => T3,
k : (x : T1) => T2
) : (x : T1) => T5
Clearly this doesn't scale. You have to stop somewhere, and woe betide your users if they need to compose more functions than you expected.
Another option is to rewrite your code such that you only compose functions one at a time:
function compose<T, U, R>(g : (y : U) => R, f : (x : T) => U) : (x : T) => R {
return x => f(g(x));
}
This rather muddies up the calling code - you now have to write the word compose
, and its attendant parentheses, O(n) times.
compose(f, compose(g, compose(h, k)))
Function composition pipelines like this are common in functional languages, so how do programmers avoid this syntactic discomfort? In Scala, for example, compose
is an infix function, which makes for fewer nested parentheses.
f.compose(g).compose(h).compose(k)
In Haskell, compose
is spelled (.)
, which makes for very terse compositions:
f . g . h . k
You can in fact hack together an infix compose
in TS. The idea is to wrap the underlying function in an object with a method which performs the composition. You could call that method compose
, but I'm calling it _
because it's less noisy.
class Comp<T, U> {
readonly apply : (x : T) => U
constructor(apply : (x : T) => U) {
this.apply = apply;
}
// note the extra type parameter, and that the intermediate type T is not visible in the output type
_<V>(f : (x : V) => T) : Comp<V, U> {
return new Comp(x => this.apply(f(x)))
}
}
// example
const comp : (x : T) => R = new Comp(f)._(g)._(h)._(k).apply
Still not as neat as compose(f, g, h, k)
, but it's not too hideous, and it scales better than writing lots of overloads.
As of Typescript 4, variadic tuple types provide a way to compose a function, whos signature is inferred from an arbitrary number of input functions.
let compose = <T, V>(...args: readonly [
(x: T) => any, // 1. The first function type
...any[], // 2. The middle function types
(x: any) => V // 3. The last function type
]): (x: V) => T => // The compose return type, aka the composed function signature
{
return (input: V) => args.reduceRight((val, fn) => fn(val), input);
};
let pipe = <T, V>(...args: readonly [
(x: T) => any, // 1. The first function type
...any[], // 2. The middle function types
(x: any) => V // 3. The last function type
]): (x: T) => V => // The pipe return type, aka the composed function signature
{
return (input: T) => args.reduce((val, fn) => fn(val), input);
};
However there are still two drawbacks with this implementation:
e.g. The following will work at compile time and at runtime
let f = (x: number) => x * x;
let g = (x: number) => `1${x}`;
let h = (x: string) => ({x: Number(x)});
let foo = pipe(f, g, h);
let bar = compose(h, g, f);
console.log(foo(2)); // => { x: 14 }
console.log(bar(2)); // => { x: 14 }
While this will complain at runtime, but infer the signature correctly and run
let fns = [f, g, h];
let foo2 = pipe(...fns);
console.log(foo2(2)); // => { x: 14 }
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