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;
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?
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