Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Typescript: variable function arguments depending on the preceding arguments

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.

like image 777
Anton Avatar asked Jun 14 '26 10:06

Anton


1 Answers

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

like image 76
Aplet123 Avatar answered Jun 16 '26 08:06

Aplet123