'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)
}


Solution 1:[1]

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.

Solution 2:[2]

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 }

Solution 3:[3]

Here's an example of a strong typed compose function in TypeScript. It has the shortcoming of not checking each intermediate function type, but it is able to derive the arg and return type for the final composed function.

Compose Function

/** Helper type for single arg function */
type Func<A, B> = (a: A) => B;

/**
 * Compose 1 to n functions.
 * @param func first function
 * @param funcs additional functions
 */
export function compose<
  F1 extends Func<any, any>,
  FN extends Array<Func<any, any>>,
  R extends
    FN extends [] ? F1 :
    FN extends [Func<infer A, any>] ? (a: A) => ReturnType<F1> :
    FN extends [any, Func<infer A, any>] ? (a: A) => ReturnType<F1> :
    FN extends [any, any, Func<infer A, any>] ? (a: A) => ReturnType<F1> :
    FN extends [any, any, any, Func<infer A, any>] ? (a: A) => ReturnType<F1> :
    FN extends [any, any, any, any, Func<infer A, any>] ? (a: A) => ReturnType<F1> :
    Func<any, ReturnType<F1>> // Doubtful we'd ever want to pipe this many functions, but in the off chance someone does, we can still infer the return type
>(func: F1, ...funcs: FN): R {
  const allFuncs = [func, ...funcs];
  return function composed(raw: any) {
    return allFuncs.reduceRight((memo, func) => func(memo), raw);
  } as R
}

Example Usage:

// compiler is able to derive that input type is a Date from last function
// and that return type is string from the first
const c: Func<Date, string> = compose(
  (a: number) => String(a),
  (a: string) => a.length,
  (a: Date) => String(a)
);

const result: string = c(new Date());

How it Works We use a reduceRight on an array of functions to feed input through each function from last to first. For the return type of compose we are able to infer the argument type based on the last function argument type and the final return type from the return type of the first function.

Pipe Function

We can also create a strong typed pipe function that pipes data through first function then to next, etc.

/**
 * Creates a pipeline of functions.
 * @param func first function
 * @param funcs additional functions
 */
export function pipe<
  F1 extends Func<any, any>,
  FN extends Array<Func<any, any>>,
  R extends
    FN extends [] ? F1 :
    F1 extends Func<infer A1, any> ?
      FN extends [any] ? Func<A1, ReturnType<FN[0]>> :
      FN extends [any, any] ? Func<A1, ReturnType<FN[1]>> :
      FN extends [any, any, any] ? Func<A1, ReturnType<FN[2]>> :
      FN extends [any, any, any, any] ? Func<A1, ReturnType<FN[3]>> :
      FN extends [any, any, any, any, any] ? Func<A1, ReturnType<FN[4]>> :
      Func<A1, any> // Doubtful we'd ever want to pipe this many functions, but in the off chance someone does, we can infer the arg type but not the return type
    : never
>(func: F1, ...funcs: FN): R {
  const allFuncs = [func, ...funcs];
  return function piped(raw: any) {
    return allFuncs.reduce((memo, func) => func(memo), raw);
  } as R
}

Example Usage

// compile is able to infer arg type of number based on arg type of first function and 
// return type based on return type of last function
const c: Func<number, string> = pipe(
  (a: number) => String(a),
  (a: string) => Number('1' + a),
  (a: number) => String(a)
);

const result: string = c(4); // yields '14'

Solution 4:[4]

I found that it's not too hard to write a typed compose function now (TypeScript v4.1.5 and above, tested in TypeScript Playground). Here's an example. It can check each intermediate function type.

type Compose<F> =
    (F extends [infer F1, infer F2, ...infer RS] ?
        (RS extends [] ?
            (F1 extends (...args: infer P1) => infer R1 ?
                (F2 extends (...args: infer P2) => infer R2 ?
                    ([R1] extends P2 ?
                        (...args: P1) => R2 :
                        never) :
                    never) :
                never) :
            Compose<[Compose<[F1, F2]>, ...RS]>) :
        never);

type ComposeArgs<T> = Parameters<Compose<T>>;
type ComposeReturn<T> = ReturnType<Compose<T>>;

// I forget that composition is from right to left! 
type Reverse<T extends unknown[], RE extends unknown[] = []> = T extends [infer F, ...infer RS] ? Reverse<RS, [F, ...RE]> : RE;

function composeL2R<T extends Function[]>(...fns: T): (...args: ComposeArgs<T>) => ComposeReturn<T> {
    return (...args: ComposeArgs<T>): ComposeReturn<T> => fns.reduce((acc: unknown, cur: Function) => cur(acc), args);
}

function compose<T extends Function[]>(...fns: T): (...args: ComposeArgs<Reverse<T>>) => ComposeReturn<Reverse<T>> {
    return (...args: ComposeArgs<Reverse<T>>): ComposeReturn<Reverse<T>> => fns.reduceRight((acc: unknown, cur: Function) => cur(acc), args);
}

function fns(x: number): string { return `${x}0`; }
function fnn(x: number): number { return 2 * x; }
function fsn(x: string): number { return parseInt(x); }

let aNumber = compose(fsn, fns, fnn, fsn, fns, () => 1)();
let aNumberL2R = composeL2R(() => 1, fns, fsn, fnn, fns, fsn)();
let aNever = composeL2R(fnn, fsn, fns)(1);
let aNeverL2R = composeL2R(fnn, fsn, fns)(1);

Sources

This article follows the attribution requirements of Stack Overflow and is licensed under CC BY-SA 3.0.

Source: Stack Overflow

Solution Source
Solution 1
Solution 2
Solution 3 bingles
Solution 4