Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Typing compose function in TypeScript (Flow $Compose)

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)
}
like image 834
Tony Avatar asked Mar 15 '18 23:03

Tony


2 Answers

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.

like image 138
Benjamin Hodgson Avatar answered Sep 29 '22 13:09

Benjamin Hodgson


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:

  1. The compiler can not verify that the output of each function matches the input of the next
  2. The compiler complains when using the spread operator (but still successfully infers the composed signature)

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 }
like image 22
Arik Avatar answered Sep 29 '22 11:09

Arik