Using a tuple as a key in a Map in Typescript/Javascript can be a powerful

Encountered a strange bug in my code where I'm struggling to achieve constant time lookup from a Map when using a tuple as the key.

Here is an example highlighting the issue, along with the workaround I am currently using to make it function:

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 workaround:
map.forEach((value: number, key: [number, number]) => {
    if(String(key) === String([0, 0])){
        console.log(value); // prints 48
    }
})

For compilation (transpilation?), I am using:

tsc hello.ts -target es6

tsc version 2.1.6

I've experimented with different approaches to make the Map.get() method function properly, but haven't found a successful solution yet.

Answer №1

When working in JavaScript (and TypeScript), it's important to understand that no two arrays are considered equal unless they point to the same array. Even if you create a new array with the same elements, it won't be recognized as equal to an existing array.

This concept becomes particularly important when using Maps, as they rely on exact array references when looking up elements. For example, if you store a value with an array as a key in a Map, you'll only be able to retrieve the value if you use the exact same array reference as the key:

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

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

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

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

To work around this limitation, you can use strings or numbers as keys since they will always be considered equal if 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'

// Since astr and bstr are strings with the same value, they are considered equal
astr === bstr; // = true

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

Answer №2

For optimal functionality and ease of use, I have designed a custom class that leverages all the methods available in maps:

class CustomMap {
    private data = new Map<string, number>();

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

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

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

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

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

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

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

By utilizing this custom map class like the example below, you can streamline operations without the need to manually handle key conversion:

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

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

Answer №3

In certain scenarios (e.g. when the second element in the pair is dependent on the first element) utilizing a nested map might offer a solution:

// scenario: a mapping from a pair of (tableId, rowId) to the title of the row

// rather than using Map<[number, number], string> where the initial number represents
// tableId and the subsequent number signifies rowId, we could implement:
const rowTitleMap = Map<number, Map<number, string>>

const title = rowTitleMap.get(2)?.get(4) // potential output is a string or undefined

Answer №4

When it comes to Typescript compatibility and potential drawbacks, I am uncertain. However, I find this method quite straightforward and effective for maintaining keys as tuples:

const stringifyTuple = (tuple) => JSON.stringify(tuple);

const retainTupleKeys = (mapForKeying = new Map()) => {
  let keyMap = new Map();
  [...mapForKeying.keys()].forEach((key) => keyMap.set(stringifyTuple(key), key));

  return (tuple) => {
    const result = keyMap.get(stringifyTuple(tuple));
    if(result) return result;

    keyMap.set(stringifyTuple(tuple), tuple);
    return tuple;
  };
};

const runTest = () => {
  let aMap = new Map([
    [[1, 2], 'value1'],
    [[3, 4], 'value2']
  ]);
  
  const getKey = retainTupleKeys(aMap);
  
  console.log(aMap.get( getKey([1, 2]) ) === 'value1');
  
  aMap.set(getKey([5, 6]), 'value3');
  
  console.log(aMap.get( getKey([5, 6]) ) === 'value3');
  
  aMap.set(getKey([3, 4]), 'value4');
  
  console.log(JSON.stringify([...aMap]));
};

runTest();

Answer №5

One key reason for this behavior is that when comparing two different array instances with the same values, they are not considered equal because they are compared by reference:

const array1 = [0, 0]
const array2 = [0, 0]
console.log(array1 === array2)
//=> false

Map also follows this equality notion when associating keys with values:

const array1 = [0, 0]
const array2 = [0, 0]

const map = new Map([[array1, 48]])

console.log(map.get(array1))
//=> 48

console.log(map.get(array2))
//=> undefined

To address this issue, you can utilize the keyalesce function, which ensures the same key for identical value sequences:

const array1 = [0, 0]
const array2 = [0, 0]

const map = new Map([[keyalesce(array1), 48]])

console.log(map.get(keyalesce(array1)))
//=> 48

console.log(map.get(keyalesce(array2)))
//=> 48

For a deeper look at how keyalesce operates internally, check out this informative post.

Similar questions

If you have not found the answer to your question or you are interested in this topic, then look at other similar questions below or use the search

After refreshing the page in Next JS, there is a delay in loading the Swiper Js styles. The Swiper slides appear stretched while waiting for Next JS to load the styles. Any suggestions

Having an issue with my Next 14.0.3 app and tailwind CSS. I recently installed swiper JS version 11.0.5 using npm. The problem arises when I reload the page, it takes about 1 or 2 seconds for the swiper styles to load. During this time, the swiper slides s ...

How can I use AJAX to update a DIV when an image is swapped out?

I run an online radio station and I've been looking for a way to display album artwork for each song that plays. After setting up the ability to automatically upload the image of the currently playing song as "artwork.png" to a web server via FTP, I c ...

React's router activeClassName feature fails to apply the active class to child routes

<ul className="right hide-on-med-and-down"> <li><IndexLink to="/" activeClassName="active">ABOUT</IndexLink></li> <li><Link to="blog" activeClassName="active">BLOG</Link></li> <li><Link t ...

Modify path and refresh display upon ajax call to node server

Recently, I made the decision to utilize a Node server as a proxy for making API calls to third-party public APIs from my front end. After successfully sending a request to my Node endpoint and then to the third-party API, I received the expected response. ...

selecting a radio button and saving its value into a JavaScript variable

How can I assign the return value from a JavaScript script function to a variable inside the HTML body? The function will return the selected variable, but how can I assign it to a variable within my HTML body? <body> <form action="somepage.php ...

Can Google Maps be initialized asynchronously without the need for a global callback function?

Currently, I am working on developing a reusable drop-in Module that can load Google Maps asynchronously and return a promise. If you want to take a look at the code I have constructed for this using AngularJS, you can find it here. One issue I have enco ...

replace the standard arrow key scrolling with a new custom event

Currently, I am in the process of creating a unique scroll box to address my specific needs. The standard html <select> box is not cutting it for me as I require two columns displayed, which I couldn't achieve with a regular <select>. Howe ...

Activate scroll event when image src changes in Angular using jQuery

Seeking assistance with utilizing jQuery to scroll to a specific thumbnail inside a modal when left/right arrows are pressed. The modal should appear when the user clicks on an image. I've managed to implement scrolling upon clicking a thumbnail, but ...

Event delegation will be ineffective when the target element is nested within another element

After receiving a recommendation from my colleagues on Stackoverflow (mplungjan, Michel), I implemented the event delegation pattern for a comment list. It has been working well and I am quite excited about this approach. However, I have encountered an iss ...

Creating a Vue.js project with Viddler's top-notch video player for seamless playback and dynamic

Hey there fellow developers! I just dove into learning Vue.js yesterday, and I'm already hooked! I'm currently working on a small application that utilizes Vue (1.0.28) to showcase an online course. You can find a JSFiddle link at the end of thi ...

Output the JSON string retrieved from a specified URL

I'm currently working on using ajax to retrieve and parse JSON data from a specific URL. I am looking for assistance on how to store the parsed array into a variable. Any guidance or suggestions would be greatly appreciated. Thank you! function rvOff ...

Managing user input in Node.js

Users are required to input a URL in the form (e.g: "") and I need to be able to access and read the content from that URL. I am uncertain about my current method. What should I enter in the URL field below? var options = { url: '....', ...

Prevent users from copying and pasting text or right-clicking on the website

I have been struggling to completely disable the ability for users to copy and paste or select text on my website across all devices. After much searching, I came across a solution that I implemented on my site. Below the <body> tag, I inserted the ...

The SonarQube code coverage differs from that of Jest coverage

I'm struggling to grasp why SonarQube and Jest coverage results differ so significantly. SonarQube coverage resultshttps://i.sstatic.net/dodGi.png Jest coverage https://i.sstatic.net/uYKmt.png https://i.sstatic.net/rakzw.png https://i.sstatic.net/ ...

SwipeJS is not compatible with a JQuery-Mobile web application

I am currently attempting to integrate SwipeJS (www.swipejs.com) into my JQuery-Mobile website. <script src="bin/js/swipe.js"></script> <style> /* Swipe 2 required styles */ .swipe { overflow: hidden; ...

When all y values are zero, Highcharts.js will shift the line to the bottom of the chart

Is there a way to move the 0 line on the y axis to the bottom of the chart when all values are 0? I attempted the solution provided in this post without success. Highcharts: Ensure y-Axis 0 value is at chart bottom http://jsfiddle.net/tZayD/58/ $(functi ...

Tips for managing mouse over events in legends on highcharts

I have successfully implemented mouseover/mouseout event handling for donut slices. Please review my code below: http://jsfiddle.net/nyhmdtb8/6/ Currently, when I hover over a slice, it highlights that slice and greys out all others. Is it possible to ac ...

What are the steps to ensure availability validation with jQuery validation?

I need assistance with validating post availability in the database using jQuery validation functionality. I have already implemented validation using the code below, but I am unsure where to place an Ajax validation before submitting the form. $(document ...

Dropzone JavaScript returns an 'undefined' error when attempting to add an 'error event'

When using Dropzone, there is a limit of 2 files that can be uploaded at once (maxFiles: 2). If the user attempts to drag and drop a third file into the Dropzone area, an error will be triggered. myDropzone.on("maxfilesexceeded", function(file){ a ...

Implementing setState callback in TypeScript with React Hooks

Hello everyone! I am currently working on defining my component types using TypeScript, and I am faced with a challenge. I am trying to set it up so that I can either pass an array of objects or a callback function containing the previous state. Below is t ...