11

In my application, I need a map of objects per date. As typescript has both a Map and a Date objects, I expected this to be as easy as.

  let map: Map<Date, MyObject> = new Map<Date, MyObject>();

And use set to add new keys and values pairs. But then I've realized that I cannot get the values using the dates, unless I use the exact same instance of Date.

I've written a unit test with it:

  it('Should not be a problem', () => {
      let d1: Date = new Date(2019, 11, 25);    // Christmas day
      let d2: Date = new Date(2019, 11, 25);    // Same Christmas day?

      let m: Map<Date, string> = new Map<Date, string>();
      m.set(d1, "X");

      expect(m.get(d1)).toBe("X");   // <- This is OK.
      expect(m.get(d2)).toBe("X");   // <- This test fails.
  });

Why is it that I can't get the value from the map unless I use the exact same instance?

jmgonet
  • 1,134
  • 18
  • 18
  • 2
    I think the line `m.get(d2)` is not gonna work since the Map stores each Date key as separate objects, no matter if they are the same Date instance. I would recommend converting the date instances into string (use method `toDateString()` for example) and you will get it. – KingDarBoja Dec 15 '19 at 17:22
  • You mean that there is no equality between Dates? – jmgonet Dec 15 '19 at 17:46
  • Not really as they are objects and equality operator check if they are the same instance. Take a look at [this post](https://stackoverflow.com/questions/492994/compare-two-dates-with-javascript) – KingDarBoja Dec 15 '19 at 17:53

3 Answers3

11

It will always be falsy because those two date objects are distinct mutable javascript objects.

new Date(2019, 11, 25) === new Date(2019, 11, 25) ==> false.

You can look at answers in this post.

Julien
  • 119
  • 6
5

This is core logic of Map, as you know map stores value in key value pair.

For the comparison of keys, the keys should always be of same reference. As you might know that string literal references are equal in many programming languages, hence use of string is preferred as key in map.

The above behavior is not only true for date but it is true for any other mutable object type.

e.g.

let str1 = 'test';
let str2 = 'test';
str1 == str2; // true

let str1 = new String('test');
let str2 = new String('test');
str1 == str2; // false

While getting value from map, the keys data is not considered, rather the unique identity of key is search. And when you create immutable object each object may have same data but the references of each object will be different. Hence it will be treated as different keys.

The solution is use types which can have same references throughout program, such as string literals.

One more example,

class Demo {
  field: string;
  constructor(f: string) {
    this.field = f;
  }
}

const d1 = new Demo('test');
const d2 = new Demo('test');

// both objects seems same by data, but there references are different
// hence will be treated as separate keys.
const m: Map<any, string> = new Map<any, string>();
m.set(d1, 'value1');

console.log(m.get(d1)); // value1
console.log(m.get(d2)); // undefined
Plochie
  • 3,944
  • 1
  • 17
  • 33
  • 3
    The issue isn't mutability, but equality. Mutability only matters in implementations of map where the equality check depends on data that can be changed. JavaScript's `Map` uses reference equality, which doesn't change even if an object is mutated, so you can have mutable object keys if you want without breaking a `Map`. Unfortunately, it also means you have to hold onto object keys when you use a `Map`, since there's no way to get a new object that's equal to an old one. – jcalz Dec 15 '19 at 18:27
  • 1
    @jcalz agreed. updated my answer. Do you feel now it makes sense? – Plochie Dec 15 '19 at 18:31
  • Yeah, thanks. In case anyone cares what I mean by holding onto mutable keys, [here's an example](http://www.typescriptlang.org/play/#code/JYOwLgpgTgZghgYwgAgArQM4HsTIN4CwAUMqciHALYQBcyGYUoA5gDTFnJzO3kCulAEbRiAXw7EEOBlwA2wJHXRRsuALz4OZCtToAiAILyke9iTLdeAZgAMY4pOlhkgrIKWYcyDYXOkdvHoAQm6mWqSWdFYArPZExDB8IAhgwF4AFnAADlkAnkHAYOkAJnC5ABRZniAeKjgAlJp+yFV1IAB0lgDUXQDccY4gMiAKANYBGN7kEADuyACy2QA8yqqs9IwsAHzl9f1EIwjjVBAY7RgQYOVwxhDreou3envEh8fUZxdXroL3AMqyUIveJEKRDLCyCDtQHMa63To8PbIAD0yOQtkG2Eh0KwsLeE3aPCuNwUEHqSNRyAeJJMxEyOXyhRKZThpOBYKxUJhrKQCLJvRRaKsAEZMRCubjyviTmciTyyRS0dSng4Qar1WrNUA). – jcalz Dec 15 '19 at 18:35
3

Better use primitive values (number, string) as Map keys:

let m: Map<string, string> = new Map<string, string>();

let d1: Date = new Date(2019, 11, 25);    // Christmas day
let d2: Date = new Date(2019, 11, 25);    // Same Christmas day?

m.set(d1.toDateString(), "X");

console.log(d1.toDateString())
console.log(d2.toDateString())
console.log(m.get(d2.toDateString()))

I provided a link to such behaviour on the above comments.

KingDarBoja
  • 1,033
  • 6
  • 12