How do I check for equality and sameness on class instances with respect to usage of ES6's Set collection?

1.2k views Asked by At

I am trying to use ES6 from TypeScript via lib.es6.d.ts. I need to understand how to enforce equality comparison and sameness for an object for use in a Set<T>. For example, the object looks like the following.

class Product {
 metadata: Map<String, String>

 constructor(id: number) { 
  metadata = new Map<String, String>();
 }

 public addMetadata(key: String, val: String): Product {
  this.metadata.set(key, val);
  return this;
 }
}

Note that id field value in Product is what determines its uniqueness. If two Product instances have the same id, they are considered the same in my application, even if the metadata differs. In general, I only want only a subset of the fields to be used as a part of testing for equality and sameness.

In Java, we override the equals method to control and test for sameness. In JavaScript, what do we need to do to determine sameness?

The link on MDN states the following:

Because each value in the Set has to be unique, the value equality will be checked.

I assume by value equality they mean ===? Again, MDN shows 4 equality algorithms in ES2015 alone.

Basically, in my class above, I want to do something like the following.

let p1 = new Product(1);
let p2 = new Product(2);
let p3 = new Product(1); //duplicate of p1 by id

p1.addMetadata('k1','v1').addMetadata('k2','v2');
p2.addMetadata('k1','v1');

let mySet = new Set<Product>();
mySet.add(p1);
mySet.add(p2);
mySet.add(p3);

assertEquals(2, mySet.size); //some assertion method
3

There are 3 answers

0
jfriend00 On

A Set or Map considers a key that is an object to be the same only if it is exactly the same object, not a different object with the same content, but the same actual object. In other words, it works just like obj1 === obj2.

If you're using a Set like your title and first paragraph refer to, then you're pretty much out of luck. Two separate objects that the same contents (or in your case, the same .id property) will be considered separate items in the Set because they are actually different objects.

You can see in this question How to customize object equality for JavaScript Set a discussion about whether this is customizable for a Set or not (it's not).


If you're using a Map (which appears to be what your code refers to, even though that's not what the text of your question says), then you could use the .id property as the key and the object itself as the value. As long as the .id property is a primitive (like a string or a number), then you will only get one item in the Map for any given id.

4
georg On

Not directly answering your question, but "tagged sets" (for a lack of a better name) might work better than overridden equality operators. This way, "equality" is an attribute of the set itself, not of underlying objects. Here's a JS example:

class SetBy extends Set {

    constructor(pred) {
        super();
        this.pred = pred;
        this.inner = new Set();
    }

    has(obj) {
        return this.inner.has(this.pred(obj));
    }

    add(obj) {
        if (!this.has(obj)) {
            this.inner.add(this.pred(obj));
            super.add(obj);
        }
    }
}


s = new SetBy(x => x.id);

a = {id: 1, name: 'a'};
b = {id: 1, name: 'b'};
c = {id: 1, name: 'c'};
d = {id: 2, name: 'd'};
e = {id: 2, name: 'e'};

s.add(a);
s.add(b);
s.add(c);
s.add(d);
s.add(e);

console.log([...s]);

4
Estus Flask On

The simple (and also performant) way to check if objects are equal is to compare JSON.stringify() strings. Due to the fact that only own enumerable properties are stringified, metadata property should be non-enumerable:

class Product {
 metadata: Map<String, String>

 constructor(public id: number) { 
     Object.defineProperty(this, 'metadata', {
         configurable: true,
         writable: true
     })

     this.metadata = new Map<String, String>(); 
 }
 ...
}

This will result in:

new Product(1) !== new Product(1);
JSON.stringify(new Product(1)) === JSON.stringify(new Product(1));
JSON.stringify(new Product(1)) !== JSON.stringify(new Product(2));

This approach could be utilized in custom Set:

class ObjectedSet extends Set {
  protected _find(searchedValue) {
    for (const value of Array.from(this.values()))
      if (JSON.stringify(value) === JSON.stringify(searchedValue))
        return searchedValue;
  }

  has(value) {
    return !!this._find(value);
  }

  add(value) {
    if (!this.has(value))
      super.add(value);

    return this;
  }

  delete(value) {
    const foundValue = this._find(value);

    if (foundValue)
      super.delete(foundValue);

    return !!foundValue;
  }
}