typescriptfunctional-programmingtypescript-generics

Imprecise type inference of pipe and compose functions in TypeScript


I have defined pipe and compose functions, each able to compose exactly two functions into a new one, with the only difference of the call order upon application to the argument.

Both functions are generic and I was hoping I can make the product-function to be strongly-typed, yet something is off and the result function type is inferred incorrectly.

Questions:

  1. Is it a limitation of the language or I am doing something wrong?
  2. How do I fix it, if it's possible in TypeScript?
function pipe<X, Y, Z>(first: (x: X) => Y): (f2: (y: Y) => Z) => (x: X) => Z {
  return function (second: (y: Y) => Z): (x: X) => Z {
    return function (x: X): Z {
      return second(first(x));
    };
  }
}

function compose2<X, Y, Z>(second: (y: Y) => Z): (f2: (x: X) => Y) => (x: X) => Z {
  return function (first: (x: X) => Y): (x: X) => Z {
    return function (x: X): Z {
      return second(first(x));
    };
  }
}

Usage

function numToStr(n: number): string { return n.toString(); }
function strToLen(s: string): number { return s.length; }

// Inferred as (x: number) => unknown
// Should be (x: number) => number
const f1 = pipe(numToStr)(strToLen);

// Inferred as(x: unknown) => number
// Should be (x: number) => number
const f2 = compose(strToLen)(numToStr);

Solution

  • Instead of hard coding the return type of each function. Allow typescript to infer it and ensure the generic parameters are used not too early:

    function pipe<X, Y>(first: (x: X) => Y) { // <-- Avoid <Z> here is too early
      return function <Z>(second: (y: Y) => Z) { // <-- <Z> is needed here
        return function (x: X) {
          return second(first(x));
        };
      }
    }
    
    function compose<Y, Z>(second: (y: Y) => Z) {// <-- Avoid <X> here is too early
      return function <X>(first: (x: X) => Y) {// <-- <X> is needed here
        return function (x: X): Z {
          return second(first(x));
        };
      }
    }
    
    function numToStr(n: number): string { return n.toString(); }
    function strToLen(s: string): number { return s.length; }
    
    const f1 = pipe(numToStr)(strToLen);
    const f2 = compose(strToLen)(numToStr);
    

    The problem is that in your first call pipe(numToStr) TypeScript can infer x from n as number and Y from the return as string but there is no Z at this point so it is infered as unknown. TypeScript don't try to re-infer it later when the second call happens, it stays as unknown.