Skip to content

How to use `(K | V)[][]` as `ReadonlyArray` in a Map?

I would like to use a (K | V)[][] as ReadonlyArray<[K, V]> in a Map constructor. Lets say that our V is an interface IItem and K a basic number.

interface IItem {
    key: number;
    val: string;
}

const items = [[1, {key: 1, val: "a"}]] 
const ze_map = new Map<number, IItem>(items);
console.log(JSON.stringify([...ze_map]));

By default, typescript will perceive the type of items as : (number | IItem)[][]. This will not work and throw an error:

Argument of type ‘(number | { key: number; val: string; })[][]’ is not assignable to parameter of type ‘ReadonlyArray<[{}, {}]>’.
Type ‘(number | { key: number; val: string; })[]’ is missing the following properties from type ‘[{}, {}]’: 0, 1

Luckily for us we can force the type to Array<[number, IItem]> to please Map.

const items2 : Array<[number, IItem]> = [[1, {key: 1, val: "a"}]] 
const ze_map2 = new Map<number, IItem>(items2);
console.log(JSON.stringify([...ze_map]));

This work as expected. Alright, lets move to my problem. What if we cannot force the type?

const arr = [
    { key: 3, val: 'bar' },
    { key: 5, val: 'world' }
];
const result: Array<[number, IItem]> = arr.map(i => ([i.key, i]));

const ze_map3 = new Map(result);
console.log(JSON.stringify([...ze_map]));

This will not work because here (number | { key: number; val: string; })[][] is not Array<[number, IItem]>. So, how to use (K | V)[][] as ReadonlyArray<[K, V]> in a Map?

You can try all the code online

I read both How to define Map with correlation between a key type and a value type, while they both are unions and Typescript Map<enum, set<enum>> “No overload matches this call”, but I don’t get why? without finding the solution. I may missed the solution.

I also read the MDN entry on Map which say that it should work as expected and if you try in vanilla JS it does work as expected:

var arr = [
    { key: 3, val: 'bar' },
    { key: 5, val: 'world' }
];

var result = arr.map(i => [i.key, i]);
const ze_map = new Map(result);

console.log(JSON.stringify([...ze_map])); 

Try it online!

Answer

The problem is the body of the .map callback. Whenever a function returns an array, Typescript will interpret the return type as a normal array type rather than a tuple type.

Avoid as

The as assertion will work, but it can be dangerous because it tells Typescript “consider this type as [number, IItem] even if it really isn’t” rather than telling Typescript “I expect this type to be [number, IItem] so please make sure that it is.” In your situation the as is not required because it really is type [number, IItem]. We just need to get typescript to interpret it that way.

Set a Generic

The easiest way to do this is by setting the generic type parameter on the .map function. The generic variable here sets the return type of the callback, so we set it to [number, IItem].

const result = arr.map<[number, IItem]>(i => [i.key, i]); // result has type [number, IItem][]

const ze_map = new Map(result); // no errors :)

Externalize the Callback

Another approach is to define your map callback i => [i.key, i] as its own function so that you can annotate the return type.

const toTuple = (i: IItem): [number, IItem] => [i.key, i];

const result = arr.map(toTuple); // result has type [number, IItem][]

const ze_map = new Map(result); // no errors :)

Typescript Playground Link