Search code examples
typescript

How to infer the array element type of each rest parameter, and create a tuple type from it?


I have taken a zip generator function, and I'm trying to infer the result type. Given zip(A, B) with A Iterable<string> and B Iterable<number> I'd want to infer the return type Generator<[string, number][]>

Here's what I got so far:

declare function zip<T extends Iterable<any>[]>(...iterables: T): Generator<T>:

The problem is that it infers a type union Generator<[string[], number[]]> instead.

The closest I think I got is something like this:

declare function zip<
  T extends Iterable<any>[],
  K = T extends Iterable<infer K>[] ? K : never,
>(...iterables: T): Generator<[...K[]]>

but that still just yields a Generator<(string | number)[]>, trying to infer it on the spot I just get Generator<any[]>:

declare function zip<T extends Iterable<any>[]>(
  ...iterables: T
): Generator<[...(T extends Iterable<infer K>[] ? K : never)]>

My desired output is:

declare const A: string[];
declare const B: Iterable<number>;
const z = zip(A, B);
//    ^? const z: Generator<[string, number][]>

Solution

  • I would be inclined to use the following call signature:

    declare function zip<T extends any[]>(
        ...iterables: { [I in keyof T]: Iterable<T[I]> }
    ): Generator<T[]>;
    

    Here the generic type parameter T corresponds to the element type of the expected output, so in your zip(A, B) example, where A is Iterable<X> and B is Iterable<Y>, then T would be [X, Y].

    Then the iterables rest parameter is of the mapped array/tuple type { [I in keyof T]: Iterable<T[I]> }. A mapped array/tuple turns arrays/tuples into arrays/tuples and it essentially iterates only over the numeric/numeric-like indexes of the array/tuple. Meaning that you can think of the I in keyof T as iterating over "0", "1", etc., up to the largest index of the tuple. Thus {[I in keyof T]: Iterable<T[I]>} will turn a tuple of types into a tuple of iterables of those types (e.g., [X, Y] becomes [Iterable<X>, Iterable<Y>]). Because it's a homomorphic mapped type (see What does "homomorphic mapped type" mean?), TypeScript is able to infer T from { [I in keyof T]: Iterable<T[I]> }.

    Let's test it out:

    declare const A: string[];
    declare const B: Iterable<number>;
    const z = zip(A, B);
    //    ^? const z: Generator<[string, number][]>
    

    Looks good. Given the rest input argument of type [string[], Iterable<number>], the compiler is able to infer T as [string, number], and then the output is of type Generator<[string, number][]>, as desired.

    Playground link to code