typescriptfunctional-programmingtypescript-genericsmapped-types

Mapped type is not an array type


I apologise for the rather undescriptive title, I'm not sure how I should properly title this question.

(Hopefully this isn't one of those XY problems...)

I encountered this error whilst trying to write a generic zipN function in TypeScript.

type Keys<T> = { [K in keyof T]: K };
type Longest2<T extends unknown[], U extends unknown[]> = Keys<T> extends [...Keys<U>, ...infer _] ? T : U;
type LongestN<T extends unknown[][]> = T extends [infer T0 extends unknown[], ...infer Ts extends unknown[][]] ? Longest2<T0, LongestN<Ts>> : T[0];
type OnlyNumeric<T> = T extends `${number}` ? T : never;
type MaybeIndex<T, K> = K extends keyof T ? T[K] : never;
type ZipN<T extends unknown[][]> = { [K in OnlyNumeric<keyof LongestN<T>>]: { [L in keyof T]: MaybeIndex<T[L], K> } };

export const zipN = <T extends unknown[][]>(...[head, ...tail]: T): ZipN<T> => [head, ...zipN(...tail) as ZipN<typeof tail>] as any;

+ Playground link

In the last line, TypeScript complains that Type 'ZipN<unknown[][]>' is not an array type..

Note the OnlyNumeric type definition. I applied it to keyof LongestN<T> in order to get rid of all of the prototype methods and things like that because those are otherwise added to the object that ZipN produces.

I specifically said object in the previous sentence, because instead of a proper array type it produces an object with numeric keys. This is obviously what is causing the error I mentioned before, however I am really quite confused about why this is happening, especially since { [L in keyof T]: MaybeIndex<T[L], K> } does yield an array (or tuple) type.

Am I doing something stupid?


Solution

  • You've run into the TypeScript bug/limitation described at microsoft/TypeScript#27995. The support for mapping array/tuple types to other array/tuple types only works when the array/tuple type whose properties you're iterating over is a bare generic type parameter. Since LongestN<T> is not such a generic type parameter, the mapped type maps over all the properties, including the array methods, and produces a non-array object.

    It's quite a longstanding issue and it's not clear when or if it will ever be addressed. Luckily you can work around it by refactoring to iterate over the keys of a generic type parameter LNT instead of LongestN<T>. There are various ways to do that. Here's one:

    type ZipN<T extends unknown[][]> = LongestN<T> extends infer LNT ?
        { [K in keyof LNT]: { [L in keyof T]: MaybeIndex<T[L], K> } } : never;
    

    This uses conditional type inference to "copy" LongestN<T> into a new type parameter LNT.

    You can verify that this produces actual array/tuples now:

    type Z = ZipN<[[0, 1, 2], [3, 4, 5]]>;
    // type Z = [[0, 3], [1, 4], [2, 5]]
    

    And your error about ZipN<T> not being an array goes away:

    const zipN = <T extends unknown[][]>(...[head, ...tail]: T): ZipN<T> =>
        [head, ...zipN(...tail) as ZipN<typeof tail>] as any; // okay
    

    Playground link to code