Use Generics to get a typed result of my little compose function

Viewed 514

My compose function is actually very simple:

const compose = (outer: any, inner: any) => (...innerParams: any) =>
  outer(inner(...innerParams));

When I now do this:

const theInner = (p1: string, p2: string): string => [p1, p2].join(' ');
const theOuter = (p1: string): number => p1.length;

const theComposed = compose(theOuter, theInner);
const theResult = theComposed('asdf', 'asdf');

VSCode's type hint shows me const theComposed: (...innerArgs: any) => any when I hover on theComposed and on theResult I get const theResult: any.
So all the types are lost.

I've been trying out several ways to get this done with Generics, but unfortunately I seem to be unable to wrap my head around this.

This is one of my failures:

export const compose = <
  T extends CallableFunction,
  U extends CallableFunction,
  PT extends ThisParameterType<U>
>(
  outer: T,
  inner: U
) => (...args: PT[]) => outer(inner(args));

And this results in theComposed showing const theComposed: (...args: unknown[]) => any and theResult is still any.
And the most things I tried for the return type were anyway only driving the typescript compiler going mad at me.

The desired result would be that I get:

const theComposed: (p1: string, p2: string) => number

Is that even doable?

TIA.

https://stackblitz.com/edit/typescript-wut8jg

1 Answers

How's this?

type InnerFunction<T extends unknown[], V> = (...args: T) => V; 
type OuterFunction<V,W> = (arg: V)=> W; 

const compose = <T extends unknown[], V, W>(
    outer: OuterFunction<V, W>, 
    inner: InnerFunction<T,V>
) => (...innerParams: T) =>
  outer(inner(...innerParams));

const theComposed = compose(theOuter, theInner);
const theResult = theComposed('asdf', 'asdf');

Playground Link

Explanation

Of course your first approach, with declaring any types isn't going to work, as you are explicitly type widening, and as you say, the problem is that the type information is lost.

So using generics, the way I've found easiest to do it, is to declare a couple of intermediate types first, and then use those types to create the compose function's types.

So first:

type InnerFunction<T extends unknown[], V> = (...args: T) => V; 

Here, we are saying that the InnerFunction type is a function and it has unknown arguments, and returns a value of type V.

type OuterFunction< V,W> = (arg: V)=> W; 

Here we are saying that the OuterFunction type is a function, and it takes a type V (by convention I've just used the same letter as the return type for inner, but that doesn't have any effect), and returns a value of type W.

Putting this together, we are just declaring that compose function with our inner and outer functions, and we just need to make all of the generic parameters available to it.

To do this, we just declare them in the angle brackets before the function's round brackets.

const compose = <T extends unknown[], V, W>(
    outer: OuterFunction<V, W>, 
    inner: InnerFunction<T,V>
)

Now, the important part here, is that the V generic parameter we have declared, is shared across both the outer function and inner function. That's how we keep the inner function's return value the same type as the outer function's argument.

Related