3

This question must have been asked before, I'm almost certain of it. Yet:

  1. I can't find any such question
  2. Typescript has made major leaps in the recent past, thus the existing answers might be outdated.

Something that I commonly use in my code is spread operator for function arguments allowing me to take in a variable length array of arguments. What I'm trying to do now is create TS type defenition for a function where arg1 type depends on arg2 type, and arg2 depends on arg3, and so on.

Very much like this in lodash https://github.com/DefinitelyTyped/DefinitelyTyped/blob/master/types/lodash/common/util.d.ts#L209

 flow<R2, R3, R4, R5, R6, R7>(f2: (a: ReturnType<T>) => R2, f3: (a: R2) => R3, f4: (a: R3) => R4, f5: (a: R4) => R5, f6: (a: R5) => R6, f7: (a: R6) => R7): Function<(...args: Parameters<T>) => R7>;

The lodash approach is obviously very limited and also maxes out (in this example) at 8 arguments. This is fine, and I can live with it, however, in 2021 is there a better, more recursive way? A way to do the same thing for a (theoretically) infinite number of arguments?

Please feel free to close the question and point me to an existing answer, if such exists and it's up to date.

1
  • 2
    I would consider changing one of the five tags to "algorithm" to increase relevant audience. Commented Jan 10, 2021 at 13:27

2 Answers 2

4

With inference in conditional types, this is possible (albeit slightly ugly):

// convenience alias
type Func = (...args: any[]) => any;

// check arguments in reverse
// since typescript doesn't like deconstructing arrays into init/last
type CheckArgsRev<T extends Func[]> =
    // get first two elements
    T extends [infer H1, infer H2, ...infer R]
        // typescript loses typings on the inferred types
        // so this gains them back
        ? H1 extends Func ? H2 extends Func ? R extends Func[]
            // actual check
            // ensures parameters of next argument extends return type of previous
            // you can substitute this with whatever check you want to add
            // just know that H1 is the current argument type and H2 is the previous
            // also if you change this to work with non-functions then change Func to an appropriate type
            // like unknown to work with all types
            ? Parameters<H1> extends [ReturnType<H2>]
                // it was a match, recurse onto the tail
                ? [H1, ...CheckArgsRev<[H2, ...R]>]
                : never // invalid type, become never for error
            : never : never : never // should never happen
        // base case, 0 or 1 elements should always pass
        : T;

// reverse a tuple type
type Reverse<T extends unknown[]> =
    T extends [infer H, ...infer R]
        ? [...Reverse<R>, H]
        : [];

// check args not in reverse by reversing twice
type CheckArgs<T extends Func[]> = Reverse<CheckArgsRev<Reverse<T>>>;

// make sure the argument passes the check
function flow<T extends Func[]>(...args: T & CheckArgs<T>) {
    console.log(args);
}

// this is invalid (since number cannot flow into a string)
flow((x: string) => parseInt(x, 10), (x: string) => x + "1");
// this is valid (number flows into number)
flow((x: string) => parseInt(x, 10), (x: number) => x + 1);

Playground link

Sign up to request clarification or add additional context in comments.

Comments

2

Aplet's answer is great! But it gives up the type information about the function array when you pass a wrong argument. An error that just says Argument of type '(x: string) => number' is not assignable to parameter of type 'never'. when the error is actually originating from (x: string) => string isn't super helpful.

Unfortunately there doesn't really seem to be a robust method of doing this, but I'd still like to share my 2 cents-

type AnyFunc = (a: any) => any;

// Given a tuple of functions - deduce how much of it is flowable
type FlowFrom<T extends readonly AnyFunc[]> = T extends [(a: infer R) => any, ...AnyFunc[]]
    ? // Infer the argument type of the first function and pass it to `FlowFrom$`
      FlowFrom$<R, T>
    : // T was empty array
      [];

// Actual function that deduces how much of a tuple is flowable, taking previous return type and remaining functions
type FlowFrom$<R, T extends readonly [(a: R) => any, ...AnyFunc[]]> = T extends [
    (a: R) => infer R$,
    ...infer Tail
]
    ? Tail extends [(a: R$) => any, ...AnyFunc[]]
        ? // Valid, continue
          [(a: R) => R$, ...FlowFrom$<R$, Tail>]
        : // Tail has either been exhausted or invalid function has been found
        Tail extends [(a: any) => any, ...AnyFunc[]]
        ? // Invalid function found, append a correct function type so it points out the specific error
          // But make the appended function optional, the flow is still valid before this point
          [(a: R) => R$, (a: R$) => any] | [(a: R) => R$]
        : // Tail exhausted
          [(a: R) => R$]
    : // T is empty, exhausted tuple (impossible - apparently)
      [];

FlowFrom deduces a "flowable" tuple type from a given tuple of functions. Essentially, it extracts all the functions in the tuple that will flow into each other, stopping whenever it encounters a function that is no longer flowable or when the tuple has exhausted.

// All the functions in the tuple type are flowable - the return type is the same
let valid: FlowFrom<[(x: string) => number, (x: number) => number]>;
//  ^ [(a: string) => number, (a: number) => number]

// First function won't flow into the second one - stop at first one
let invalid: FlowFrom<[(x: string) => number, (x: string) => number]>;
//  ^ [(a: string) => number, (a: number) => any] | [(a: string) => number]

This means, when you try to assign [(x: string) => parseInt(x, 10), (x: string) => x + '1'] to invalid, you get a nice error-

let invalid: FlowFrom<[(x: string) => number, (x: string) => number]> = [
    (x: string) => parseInt(x, 10),
    (x: string) => x + '1',
];
//  ^ Type 'number' is not assignable to type 'string'

But, you can still assign the stuff before that breaking point-

let reducedButCorrect: FlowFrom<[(x: string) => number, (x: string) => number]> = [
    (x: string) => parseInt(x, 10),
];

(in case you don't want this behavior, as in - being able to still assign everything before the breaking point when a breaking point is present, remove the union type from FlowFrom$, [(a: R) => R$, (a: R$) => any] | [(a: R) => R$] -> [(a: R) => R$, (a: R$) => any])

You can now use it like-

declare function flow<T extends readonly AnyFunc[]>(...args: T & FlowFrom<T>): unknown;

// Valid
flow(
    (x: string) => parseInt(x, 10),
    (x: number) => Boolean(x),
    (x: boolean) => Number(x)
);

// Invalid
flow(
    (x: string) => parseInt(x, 10),
    (x: number) => Boolean(x),
    (x: string) => Number(x)
);
//  ^ Type 'boolean' is not assignable to type 'string'

Try all of this out on playground

Just wanted to share my two cents.

1 Comment

Thank you for your answer, you provide a great implementation. I have similar implementation to yours, but it doesn't work if using one Array parameter instead of using spread parameters. I add some code below your code: shorturl.at/pwzIR

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.