2

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
Jane Wayne
  • 6,828
  • 9
  • 57
  • 97
  • I don't have a good answer but can clear some things up. In javascript "==" or "===" for non-primitives is reference equality. So it has to be the exact same instance to evaluate to "true" (doesn't check properties at all). I haven't looked much into "Set" and "Map" yet but hopefully this helped. – Adrian Dec 15 '16 at 05:25
  • 1
    Possible duplicate of [How to customize object equality for JavaScript Set](http://stackoverflow.com/questions/29759480/how-to-customize-object-equality-for-javascript-set) – Adrian Dec 15 '16 at 05:29
  • @Aliester - As the one who provided an answer to that other question, I don't really think this is a dup. The question here is completely different. Reading that other answer probably will give you some info one could use to answer this question, but there's no way anyone would say this question is the same as that question. – jfriend00 Dec 15 '16 at 05:32
  • @jfriend00 apologies if I misunderstood but isn't the OP is essentially asking how to use a custom javascript object with a Set and provide his own custom equality evaluation instead of the native logic? I will defer to you opinion on it however. – Adrian Dec 15 '16 at 05:37
  • @Aliester - First off, the OP is a bit confused about whether they're using a `Set` or `Map`. The text of the question refers to a `Set`, but the code refers to a `Map`. If they are using a `Map` and can just make the `id` value be the key in the `Map`, then their problem may be solved. Let's see how that plays out first. – jfriend00 Dec 15 '16 at 05:39
  • @jfriend00 good point. Flag retracted. – Adrian Dec 15 '16 at 05:41
  • @jane-wayne I wrote a library I believe may help you. Take a look at https://mfedatto.github.io/sameness.js/ – MFedatto Jun 18 '18 at 19:51

3 Answers3

2

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;
  }
}
Estus Flask
  • 150,909
  • 47
  • 291
  • 441
  • String serialisation sounds like an awfully inperformant way to do an equality comparison. Why do you think it's fast? – Bergi Dec 15 '16 at 14:41
  • @Bergi I would expect that, but in my own benchmarks `JSON.stringify` comparison performed on par with `_.isEqual`. Both were fast, so this consideration falls into preliminary optimizations category. If Lodash isn't used in project, I would consider `JSON.stringify` a no-brainer. – Estus Flask Dec 15 '16 at 15:23
  • Yeah, for small and equal inputs I'd put them on the same level. But if the two inputs share a large data structure (that `_.isEqual` could check by reference identity) or for large but unequal data structures (that `JSON.stringify` still needs to traverse completely) I'd expect major differences. – Bergi Dec 15 '16 at 17:05
  • @Bergi Sure. IIRC, stringify somewhat outperformed isEqual on equal objects and underperformed on unequal. I've had average small objects in my use case (10-20 of enumerable props in total), the performance wasn't an issue at all. – Estus Flask Dec 15 '16 at 18:02
1

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.

Community
  • 1
  • 1
jfriend00
  • 580,699
  • 78
  • 809
  • 825
1

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]);
georg
  • 195,833
  • 46
  • 263
  • 351
  • This approach misses `delete`. Implementing it is less fun than `add`. – Estus Flask Dec 15 '16 at 10:15
  • @estus: it would a one-liner either, no? – georg Dec 15 '16 at 12:22
  • @georg No, as you'd need to delete the object from the outer set that has the same predicate as the one that is getting deleted. An inner `Map` (instead of `Set`) that points back to the outer object might solve this. – Bergi Dec 15 '16 at 14:46
  • @Bergi: makes sense. What OP is actually looking for seems to be Map not Set. – georg Dec 16 '16 at 09:57