2

Given a function type F, I want to create a new type that's "composable before" F, meaning it takes and returns the same arguments that F takes. For example:

const stringToNumber = (s: string) => s.length;
const stringToString: ComposableOn<(s: string) => number> = (s: string) => s + s;

const composedResult = stringToNumber(stringToString('a'));

I've not been able to properly define the type ComposableOn, though. Here are things I've tried:

type ComposableBefore<F extends (...args: any) => any> = (args: Parameters<F>) => Parameters<F>;
type StoN = (s: string) => number;

const sToS: ComposableBefore<StoN> = (s: string) => s + s; // error: the type is [string] => [string] and not string => string
type ComposableBefore<F extends (...args: any) => any> = Parameters<F> extends Array<infer U>? (...args: Parameters<F>) => U: never;

const complex: ComposableBefore<(a: string, b: number, c: { d: number, e: string }) => number> = (a, b, c) => c; // not good either, since it can return a value of any type of the original function's argument types.

What would be a correct way to type this?

4
  • 1
    But for the multi parameter case what should the function return ? Commented Apr 11, 2019 at 16:15
  • I see now that it makes sense to return a tuple with the correct types - does this mean I would always have to wrap the return arguments in a tuple (including the single-value case), and then spread them? Like so: const sToS: ComposableBefore<StoN> = (s: string) => [s + s]; const sToN: StoN = (s) => s.length; const res = sToN(...sToS('a')); Commented Apr 11, 2019 at 16:19
  • 1
    You could have a special case for single parameter functions .. but for multiple parameters, yes you need to return a tuple and you need to spread it. The arguments don't need to be a tuple, you can use rest on the parameters: type ComposableBefore<F extends (...args: any) => any> = (...args: Parameters<F>) => Parameters<F>; Commented Apr 11, 2019 at 16:23
  • 1
    See my answer :) Commented Apr 11, 2019 at 16:33

1 Answer 1

1

You can use ... to spread the Parameters directly in the function signature. As for the return, since you want to support multiple parameters, the return type should be a tuple and thus you will need to spread the return of your function:

const stringToNumber = (s: string) => s.length;
const stringToString: ComposableBefore<(s: string) => number> = (s: string) => [s + s];

const composedResult = stringToNumber(...stringToString('a'));

type ComposableBefore<F extends (...args: any) => any> = (...args: Parameters<F>) => Parameters<F>;
type StoN = (s: string) => number;

You could also consider adding a special case for single parameter functions, if this is the common use case:

const stringToNumber = (s: string) => s.length;
const stringToString: ComposableBefore<(s: string) => number> = (s: string) => s + s;

const composedResult = stringToNumber(stringToString('a'));

const multiStringToNumber = (s: string, s2: string) => s.length + s2.length;
const multiStringToString: ComposableBefore<typeof multiStringToNumber> = (s: string, s2: string) => [s + s, s2 + s2];

const multiComposedResult = multiStringToNumber(...multiStringToString('a', 'b'));

type ComposableBefore<F extends (...args: any) => any> =
    F extends (a: infer U) => any ? (...args: Parameters<F>) => U :
        (...args: Parameters<F>) => Parameters<F>;
Sign up to request clarification or add additional context in comments.

1 Comment

That last definition of ComposableBefore is just what I needed to make the single-argument case nicer looking. Thanks!

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.