Typescript/Javascript: using tuple as key of Map


Hit this odd bug in my code and I can't figure the way to get a constant time lookup from a Map when using a tuple as my key.

Hopefully this illustrates the issue, and the workaround I'm using now just to get it to work:

hello.ts:

let map: Map<[number, number], number> = new Map<[number, number], number>()
    .set([0, 0], 48);

console.log(map.get([0,0])); // prints undefined

console.log(map.get(String([0, 0]))); //  compiler:  error TS2345: Argument of type 
// 'string' is not assignable to parameter of type '[number, number]'.

//the work-around:
map.forEach((value: number, key: [number, number]) => {
    if(String(key) === String([0, 0])){
        console.log(value); // prints 48
    }
})

To compile (transpile?) I'm using:

tsc hello.ts -target es6

tsc version 2.1.6

Tried several things to make the Map.get() method to work, not having much success.

In JavaScript (and as an extension, TypeScript), no two arrays are equal except if they refer to the same array (i.e., when changing the elements of one also would change the elements of another). If you create a new array with the same elements, it would not consider it to be equal to any existing one.

Because Maps consider such equality when looking up elements, if you store a value with an array as a key, you can only get the value out again if you pass in the exact same array reference as a key again:

const map: Map<[ number, number], number> = new Map<[ number, number ], number>();

const a: [ number, number ] = [ 0, 0 ];
const b: [ number, number ] = [ 0, 0 ];

// a and b have the same value, but refer to different arrays so are not equal
a === b; // = false

map.set(a, 123);
map.get(a); // = 123
map.get(b); // = undefined

One simple workaround for this is to use strings or numbers as keys, as these are always considered equal when they have the same value:

const map: Map<string, number> = new Map<string, number>();

const a: [ number, number ] = [ 0, 0 ];
const b: [ number, number ] = [ 0, 0 ];

const astr: string = a.join(','); // = '0,0'
const bstr: string = b.join(','); // = '0,0'

// astr and bstr have the same value, and are strings so they are always equal
astr === bstr; // = true

map.set(astr, 123);
map.get(astr); // = 123
map.get(bstr); // = 123

I would create my own class to do this so that I can easily use all of the map methods:

class MyMap {
    private map = new Map<string, number>();

    set(key: [number, number], value: number): this {
        this.map.set(JSON.stringify(key), value);
        return this;
    }

    get(key: [number, number]): number | undefined {
        return this.map.get(JSON.stringify(key));
    }

    clear() {
        this.map.clear();
    }

    delete(key: [number, number]): boolean {
        return this.map.delete(JSON.stringify(key));
    }

    has(key: [number, number]): boolean {
        return this.map.has(JSON.stringify(key));
    }

    get size() {
        return this.map.size;
    }

    forEach(callbackfn: (value: number, key: [number, number], map: Map<[number, number], number>) => void, thisArg?: any): void {
        this.map.forEach((value, key) => {
            callbackfn.call(thisArg, value, JSON.parse(key), this);
        });
    }
}

(code in playground)

As you can see, the forEach for example will automatically give you the key as [number, number] instead of a string which you will then need to parse.

Usage example:

let map = new MyMap();
map.set([1, 2], 4);
console.log(map.get([1, 2])) // 4

map.set([3, 4], 20);
map.forEach((v, k) => console.log(k, v));
// prints:
// [1, 2] 4
// [3, 4] 20