Question:
What is a Mapped type if it is not an array type in TypeScript?

Problem

I apologize 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


Suggested blogs:

>Fix: Strange compilation error with Typescript and Angular

>What if the property 'X' does not exist on type 'FastifyContext<unknown>'?

>How to get the type of a class property's getter/setter in TypeScript?

>How to assign multiple const variables in a single declaration in TypeScript?

>Type inference with 'as const' IN TypeScript

>Typescript Return argument: Tuple type or passed to rest parameter warning?

>Why Typescript does not allow a union type in an array?

>Narrow SomeType vs SomeType[]

>Create a function that convert a dynamic Json to a formula in Typescript

>How to destroy a custom object within the use Effect hook in TypeScript?

>How to type the values of an object into a spreadsheet in TypeScript?


Nisha Patel

Nisha Patel

Submit
0 Answers