0
votes

I've written this simple compose function which works just fine. However in order to assure type safety I had to resort to using generics to provide the compiler with type hints even though it's easily possible to infer the signature of "upperCaseAndLog".

const compose = <T, R>(...fns: Array<(a: any) => any>) => (a: T): R =>
  fns.reduce((b, f) => f(b), a);

const greet = (s: string) => "Hello " + s;
const toUpperCase = (s: string) => s.toUpperCase();
const log = console.log;

const upperCaseAndLog = compose<string, void>(
  greet,
  toUpperCase,
  log
);

upperCaseAndLog("bill");

Am I missing something, is there a more elegant way of accomplishing the same goal? I assume that languages like F# or Haskell would be able to infer the types without any additional information.

1
The compiler cannot infer your types because they don't appear in the parameters. How do you expect it to be able to tell? - SLaks
Type variables. - Mateja Petrovic

1 Answers

1
votes

Typescript can't infer such linked types (linked in the sense that the argument o the function depends on the result of the previous function).

You can't even define the signature of compose in a general enough way that it works for an number of function. What we can do, is define overloads that accept up to a given number of functions:

type Fn<A, R> = (a: A) => R // just to be a bit shorter in the compose signature, you can use teh function signature directly  
function compose<T, P1, P2, R>(fn1: Fn<T, P1>, fn2: Fn<P1, P2>, f3: Fn<P2, R>) : Fn<T, R>
function compose<T, P1, R>(fn1: Fn<T, P1>, f2: Fn<P1, R>) : Fn<T, R>
function compose(...fns: Array<(a: any) => any>) {
    return function (a: any) {
        return fns.reduce((b, f) => f(b), a);
    }
}

const greet = (s: string) => "Hello " + s;
const toUpperCase = (s: string) => s.toUpperCase();
const log = console.log;

const upperCaseAndLog = compose(
    greet,
    toUpperCase,
    log
);

upperCaseAndLog("bill");//(a: string) => void