271

New ES 6 (Harmony) introduces new Set object. Identity algorithm used by Set is similar to === operator and so not much suitable for comparing objects:

var set = new Set();
set.add({a:1});
set.add({a:1});
console.log([...set.values()]); // Array [ Object, Object ]

How to customize equality for Set objects in order to do deep object comparison? Is there anything like Java equals(Object)?

czerny
  • 15,090
  • 14
  • 68
  • 96
  • 5
    What do you mean by "customize equality"? Javascript does not allow for operator overloading so there is no way to overload the `===` operator. The ES6 set object does not have any compare methods. The `.has()` method and `.add()` method work only off it being the same actual object or same value for a primitive. – jfriend00 Apr 20 '15 at 22:33
  • 32
    By "customize equality" I mean any way how developer can define certain couple of objects to be considered equal or not. – czerny Apr 20 '15 at 22:46
  • 1
    Also https://stackoverflow.com/q/10539938/632951 – Pacerier Sep 19 '17 at 00:14
  • 1
    This [could be part of the *collection normalization* TC39 proposal](https://github.com/tc39/proposal-collection-normalization/issues/18) – Bergi Jul 28 '20 at 20:43

11 Answers11

150

Update 3/2022

There is currently a proposal to add Records and Tuples (basically immutable Objects and Arrays) to Javascript. In that proposal, it offers direct comparison of Records and Tuples using === or !== where it compares values, not just object references AND relevant to this answer both Set and Map objects would use the value of the Record or Tuple in key comparisons/lookups which would solve what is being asked for here.

Since the Records and Tuples are immutable (can't be modified) and because they are easily compared by value (by their contents, not just their object reference), it allows Maps and Sets to use object contents as keys and the proposed spec explicitly names this feature for Sets and Maps.

This original question asked for customizability of a Set comparison in order to support deep object comparison. This doesn't propose customizability of the Set comparison, but it directly supports deep object comparison if you use the new Record or a Tuple instead of an Object or an Array and thus would solve the original problem here.

Note, this proposal advanced to Stage 2 in mid-2021. It has been moving forward recently, but is certainly not done.

Mozilla work on this new proposal can be tracked here.


Original Answer

The ES6 Set object does not have any compare methods or custom compare extensibility.

The .has(), .add() and .delete() methods work only off it being the same actual object or same value for a primitive and don't have a means to plug into or replace just that logic.

You could presumably derive your own object from a Set and replace .has(), .add() and .delete() methods with something that did a deep object comparison first to find if the item is already in the Set, but the performance would likely not be good since the underlying Set object would not be helping at all. You'd probably have to just do a brute force iteration through all existing objects to find a match using your own custom compare before calling the original .add().

Here's some info from this article and discussion of ES6 features:

5.2 Why can’t I configure how maps and sets compare keys and values?

Question: It would be nice if there were a way to configure what map keys and what set elements are considered equal. Why isn’t there?

Answer: That feature has been postponed, as it is difficult to implement properly and efficiently. One option is to hand callbacks to collections that specify equality.

Another option, available in Java, is to specify equality via a method that object implement (equals() in Java). However, this approach is problematic for mutable objects: In general, if an object changes, its “location” inside a collection has to change, as well. But that’s not what happens in Java. JavaScript will probably go the safer route of only enabling comparison by value for special immutable objects (so-called value objects). Comparison by value means that two values are considered equal if their contents are equal. Primitive values are compared by value in JavaScript.

jfriend00
  • 683,504
  • 96
  • 985
  • 979
  • 5
    Why not implementing a simple GetHashCode or similar? – Jamby Sep 24 '16 at 15:12
  • @Jamby - That would be an interesting project to make a hash that handles all types of properties and hashes properties in the right order and deals with circular references and so on. – jfriend00 Sep 24 '16 at 20:32
  • 1
    @Jamby Even with a hash function you still have to deal with collisions. You're just deferring the equality problem. – mpen Feb 01 '17 at 22:03
  • 13
    @mpen That's not right, I'm allowing the developer to manage his own hash function for his specific classes which in almost every case prevent the collision problem since the developer knows the nature of the objects and can derive a good key. In any other case, fallback to current comparison method.[Lot](https://msdn.microsoft.com/en-us/library/system.object.gethashcode(v=vs.100).aspx) [of](https://en.wikipedia.org/wiki/Java_hashCode()) [languages](https://docs.ruby-lang.org/en/2.0.0/Hash.html) [already](https://docs.python.org/2/reference/datamodel.html#object.__hash__) do that, js not. – Jamby Feb 03 '17 at 15:26
  • Is this answer still accurate in 2017? – Jonah Jul 17 '17 at 17:52
  • @Jonah - As far as i know, this is still the case. Equality is based on being the actual same object. – jfriend00 Jul 17 '17 at 20:37
  • @jfriend00, Is it postponed to ES7 or ES8? – Pacerier Sep 18 '17 at 23:17
  • 3
    @Jamby coz js-programmers don't know about hash-code :( – Peter Dec 02 '18 at 22:26
  • I added an [answer below](https://stackoverflow.com/a/56353815/278488) explaining a simple solution using immutable-js (which also solves the mutability problem mentioned here). – Russell Davis May 29 '19 at 05:51
  • 1
    @Jamby, he's saying whenever you modify any key, the set needs to be refreshed. But I don't see why the contract needs to be so and practically speaking, no one has yet shot their toe this way in Java. – Pacerier Jun 28 '20 at 12:56
  • 1
    I don't see how the possibility of mutation is any more of a problem then coding with objects ever is. I mean `const obj={}` does not make the object immutable, yet typical code will/in-practice-must often assume that a referenced object does not change between particular points in the program, even when those points are not in the same execution slice. And then there is always deep-copy to hedge bets, on a case-by-case basis. – Craig Hicks Jan 11 '21 at 20:16
  • @jfriend00 - Of course I see your point given the criterion that you are asserting (that hashes of object in the Set are immutable - equivalently that the objects themselves are immutable). What I am saying is that code is frequently written that depends on objects deep content (or at least portions of it) remaining constant between certain non trivially distant points in the code execution. Without trusting those assumptions, programming with JS (and other languages) would not be possible. That same trust, and the risks that trust entails can be extended to sets with deep objects. – Craig Hicks Jan 13 '21 at 01:52
  • @jfriend00 - If the application defines "equivalence" as being the same object (===) then it is trivial to add an uuid to each object and use that as the index. In the non-trivial case non-identical (!==) objects can be defined as "equivalent" by the application as needed. That definition of equivalence is application specific and it is therefore impossible to define it a priori at the language level. – Craig Hicks Jan 13 '21 at 02:19
  • @jfriend00 - My argument is that your quote *"However, this approach is problematic for mutable objects: In general, if an object changes, its “location” inside a collection has to change, as well."* is irrelevant to the reason why a generalized Set for deep objects has not been implemented at the JS level. Focusing on an irrelevant reason takes attention away from the critical reasons. – Craig Hicks Jan 13 '21 at 03:03
  • 1
    JavaScript programmers still need to catch up on Java. Come on people, do you know how useful these features are for programmers? Yeah if you change one of the the key, that could mean broken contract but that's more of a documentation issue rather than NOT implementing the feature. Now we are stuck with 20 workarounds, 40 outdated ones, 20 javascript programmers discussing what the best solution is. Just implement it already – TimothyBrake Jun 14 '21 at 16:19
  • This is insane! `That feature has been postponed, as it is difficult to implement properly and efficiently` quit working on one of the most widely used languages then! This problem has been solved _so many times_. – Nearoo Feb 14 '22 at 10:29
  • @Nearoo - Are you even aware of the issues? Please point me to another implementation that puts a pointer to a mutable object in a Set, evaluates uniqueness in the Set by comparing all the properties of the object vs. all the other objects in the Set and then maintains the integrity of the Set when the data in the object is modified later? I'd be happy to see one that works that way. It would not be very difficult with immutable objects, but Javascript accepts mutable objects in a Set. – jfriend00 Feb 15 '22 at 05:18
  • @Nearoo - As such Javascript guarantees uniqueness of the object reference itself (no two of the exact same object reference in the Set), but does not attempt to compare content of the object. So, two separate objects with the same content are allowed in the Set. A Set that examines content of objects for uniqueness is a completely different thing and would require immutable objects to have any integrity since code that modified the objects that were in the Set could easily render the Set invalid. – jfriend00 Feb 15 '22 at 05:22
  • @Nearoo - In looking into how one would do this for immutable objects, I ran into more obstacles since an instance of an object can have state that is not accessible to the outside world so could not be compared directly by a Set implementation. Object state can even be in closures and there are objects that are backed by native code such as a socket object or a DOM element so that part of the state is not in Javascript - complicating how you would compare the "value" of two objects when all the state is not accessible. – jfriend00 Feb 15 '22 at 06:21
  • @jfriend00 No I agree, the concepts of mutable objects in a set is not clear. But not all objects need to be mutable. What's insane to me is that js just gives up and simply checks for object identity. A great solution is e.g. how python handles things: Every object has a `__hash__` function, which musn't change over the lifetime of the object. Dtastructures like e.g. sets consider objects equal if their hash is equal. Mutable objects are never equal, regardless of their value; but immutable types like e.g. `tuple` or frozen `dataclass` are equal iff their values are equal. – Nearoo Feb 15 '22 at 10:22
  • @jfriend00 Point is that there is currently no way in js to emulate sets that contain any container objects whatsoever. If you have a couple of containers and want to check if another containers is new, you have to do linear search with deep equality every single time, or implement your own new set that serializes every object to string every time. _That_ is insane. – Nearoo Feb 15 '22 at 10:25
  • @Nearoo - OK, so we agree on mutable objects in a Set being problematic. What immutable types do you think Javascript has that make supporting it for immutable cases interesting? – jfriend00 Feb 15 '22 at 22:01
  • @jfriend00 Frozen objects for starters, a new datatype called 'tuple', ideally: an interface "Hashable" which has a function `hash()`, to inherit before being added to a set. – Nearoo Feb 16 '22 at 14:42
  • @Nearoo - So, you think a `Set` object should behave differently if you add a frozen object vs. a non-frozen object or perhaps a different type of Set that only accepts frozen objects? The addition of a hash method for objects that the Set would use for determining uniqueness would be a welcome addition and it could have a useful default implementation that could be overriden. As for tuples, don't you pretty much get that capability with a frozen array? – jfriend00 Feb 16 '22 at 17:14
  • @Nearoo - There are edge cases to all this such as a frozen object might only be frozen at the top level (referred to as a shallow freeze). If it contains a reference to another object that isn't frozen, then the hash value of the object isn't actually frozen and could change if the nested object changes. And, there are the usual issues with circular references. These seem solvable, but I bring them up because sometimes, the devil is in the details and edge cases. I guess anything that contains references to native code stuff would have to override the `hash()` method to be used. – jfriend00 Feb 16 '22 at 17:19
  • @jfriend00 I'm aware of the edge cases, they exist in Python as well: you can still change the value of a frozen (=made hashable) `dataclass` with some magic. As @TimothyBrake said, this can be solved easily with contracts. In any case, we need _something_ because right now there's _literally nothing_. I get that this isn't a simple problem, but we should be able to expect non-simple problems solved in a language as large as javascript!!! – Nearoo Feb 16 '22 at 18:00
  • @Nearoo - I'm not plugged into why this is or isn't being working on in the Javascript community or why nothing has been done about it until now. We don't even yet have basic Set methods like union, difference, intersect, etc... which I would have thought would have been in the very first release of a Set. Those are now finally coming, but it's taken forever. – jfriend00 Feb 16 '22 at 18:06
  • 1
    @Nearoo - Added note to this answer about the [Records and Tuples proposal](https://github.com/tc39/proposal-record-tuple) which would solve most of what is being asked for here. It doesn't offer Set comparison customizability, but it allows Records or Tuples to be directly compared by their contents and allows them to be keys in a Set or Map (which would compare them by content) which would solve what was originally asked for in the question here. – jfriend00 Mar 23 '22 at 20:31
  • I like the "JavaScript will probably go the safer route" part. It's nice if JS will become a bit more safer language to use. – ruX Aug 11 '22 at 09:31
  • you need 2 functions in the set constructor: - getHash and isEqual that's it. it's not complicated – Erik Aronesty Jun 23 '23 at 20:22
  • @ErikAronesty - Until one of the objects in the Set is modified in a way that makes that object a duplicate of another object in the Set. Then, it's no longer a Set of unique objects. There's a little more to it than just a custom comparison/hash function unless the objects are immutable. – jfriend00 Jun 24 '23 at 09:57
35

As mentioned in jfriend00's answer customization of equality relation is probably not possible.

Following code presents an outline of computationally efficient (but memory expensive) workaround:

class GeneralSet {

    constructor() {
        this.map = new Map();
        this[Symbol.iterator] = this.values;
    }

    add(item) {
        this.map.set(item.toIdString(), item);
    }

    values() {
        return this.map.values();
    }

    delete(item) {
        return this.map.delete(item.toIdString());
    }

    // ...
}

Each inserted element has to implement toIdString() method that returns string. Two objects are considered equal if and only if their toIdString methods returns same value.

czerny
  • 15,090
  • 14
  • 68
  • 96
  • 3
    You could also have the constructor take a function that compares items for equality. This is good if you want this equality to be a feature of the set, rather than of the objects used in it. – Ben J Mar 02 '16 at 09:37
  • 5
    @BenJ The point of generating a string and put it in a Map is that in that way your Javascript engine will use a ~O(1) search in native code for searching the hash value of your object, while accepting an equality function would force to do a linear scan of the set and check every element. – Jamby Sep 24 '16 at 15:20
  • 3
    One challenge with this method is that it I think it assumes that the value of `item.toIdString()` is invariant and cannot change. Because if it can, then the `GeneralSet` can easily become invalid with "duplicate" items in it. So, a solution like that would be restricted to only certain situations likely where the objects themselves are not changed while using the set or where a set that becomes invalid is not of consequence. All of these issues probably further explain why the ES6 Set does not expose this functionality because it really only works in certain circumstances. – jfriend00 Jan 20 '17 at 22:02
  • The problem with this answer is that (from outward appearance) `item.toIdString()` computes the id string independent of the contents of the instance of the GeneralSet into which it will be inserted. That precludes the possibility of a hash function - therefore validating your statement about being "memory expensive". Passing the GeneralSet as a parameter - `item.toIdString(gs:GeneralSet)` enables hashes to be used. Practically speaking that's the only way to do it in the "general" case (due to memory limitations) although it is obviously more work to manage the hashing. – Craig Hicks Jan 13 '21 at 02:49
  • Actually, I take back the statement that the general set "must" be checked for collisions. With a suitable `toIdString()` string function and and a suitable hash function `hashOfIdString()`, the chance of collision is sufficiently low to that it may be ignored. And the memory usage is low - making your statement about "memory expensive" be incorrect. – Craig Hicks Jan 13 '21 at 03:20
  • it doesn't have to be a string. it can be any unique id – Erik Aronesty Jun 23 '23 at 20:23
27

As the top answer mentions, customizing equality is problematic for mutable objects. The good news is (and I'm surprised no one has mentioned this yet) there's a very popular library called immutable-js that provides a rich set of immutable types which provide the deep value equality semantics you're looking for.

Here's your example using immutable-js:

const { Map, Set } = require('immutable');
var set = new Set();
set = set.add(Map({a:1}));
set = set.add(Map({a:1}));
console.log([...set.values()]); // [Map {"a" => 1}]
Russell Davis
  • 8,319
  • 4
  • 40
  • 41
6

Maybe you can try to use JSON.stringify() to do deep object comparison.

for example :

const arr = [
  {name:'a', value:10},
  {name:'a', value:20},
  {name:'a', value:20},
  {name:'b', value:30},
  {name:'b', value:40},
  {name:'b', value:40}
];

const names = new Set();
const result = arr.filter(
  item => !names.has(JSON.stringify(item)) 
    ? names.add(JSON.stringify(item)) 
    : false
);

console.log(result);
Philippe Fanaro
  • 6,148
  • 6
  • 38
  • 76
GuaHsu
  • 309
  • 3
  • 8
  • 5
    This can work but doesnt have to as JSON.stringify({a:1,b:2}) !== JSON.stringify({b:2,a:1}) If all objects are created by your program in the same order you're safe. But not a really safe solution in general – relief.melone Nov 18 '18 at 10:48
  • 7
    Ah yes, "convert it to a string". Javascript's answer for everything. – Timmmm Dec 20 '19 at 14:59
5

To add to the answers here, I went ahead and implemented a Map wrapper that takes a custom hash function, a custom equality function, and stores distinct values that have equivalent (custom) hashes in buckets.

Predictably, it turned out to be slower than czerny's string concatenation method.

Full source here: https://github.com/makoConstruct/ValueMap

Community
  • 1
  • 1
mako
  • 1,201
  • 14
  • 30
  • “string concatenation”? Isn’t his method more like “string surrogating” (if you’re going to give it a name)? Or is there a reason you use the word “concatenation”? I’m curious ;-) – binki Jun 25 '17 at 01:54
  • @binki This is a good question and I think the answer brings up a good point that it took me a while to grasp. Typically, when computing a hash code, one does something like [HashCodeBuilder](http://commons.apache.org/proper/commons-lang/apidocs/org/apache/commons/lang3/builder/HashCodeBuilder.html) which multiplies the hash codes of individual fields and is not guaranteed to be unique (hence the need for a custom equality function). However, when generating an id string you concatenate the id strings of individual fields which IS guaranteed to be unique (and thus no equality function needed) – Pace Oct 17 '18 at 12:29
  • 1
    So if you have a `Point` defined as `{ x: number, y: number }` then your `id string` is probably `x.toString() + ',' + y.toString()`. – Pace Oct 17 '18 at 12:30
  • Making your equality comparison build some value which is guaranteed to vary only when things should be considered non-equal is a strategy I have used before. It’s easier to think about things that way sometimes. In that case, you’re generating **keys** rather than **hashes**. As long as you have a key deriver which outputs a key in a form that existing tools support with value-style equality, which almost always ends up being `String`, then you can skip the whole hashing and bucketing step as you said and just directly use a `Map` or even old-style plain object in terms of the derived key. – binki Oct 17 '18 at 15:45
  • 2
    One thing to be careful of if you actually use string concatenation in your implementation of a key deriver is that string properties may need to be treated special if they are allowed to take on any value. For example, if you have `{x: '1,2', y: '3'}` and `{x: '1', y: '2,3'}`, then `String(x) + ',' + String(y)` will output the same value for both objects. A safer option, assuming you can count on `JSON.stringify()` being deterministic, is to take advantage of its string escaping and use `JSON.stringify([x, y])` instead. – binki Oct 17 '18 at 15:58
  • Nice! Your map is more generally applicable than the czerny's approach, so it's fine when it's slower. What's strange is that you use the hash itself as a key, which makes your buckets to have nearly always length of one. Your benchmark compares two semantically different things as your `moahash` is not injective, i.e., it can't be used as `toIdString`. – maaartinus Mar 31 '20 at 01:34
4

Comparing them directly seems not possible, but JSON.stringify works if the keys just were sorted. As I pointed out in a comment

JSON.stringify({a:1, b:2}) !== JSON.stringify({b:2, a:1});

But we can work around that with a custom stringify method. First we write the method

Custom Stringify

Object.prototype.stringifySorted = function(){
    let oldObj = this;
    let obj = (oldObj.length || oldObj.length === 0) ? [] : {};
    for (let key of Object.keys(this).sort((a, b) => a.localeCompare(b))) {
        let type = typeof (oldObj[key])
        if (type === 'object') {
            obj[key] = oldObj[key].stringifySorted();
        } else {
            obj[key] = oldObj[key];
        }
    }
    return JSON.stringify(obj);
}

The Set

Now we use a Set. But we use a Set of Strings instead of objects

let set = new Set()
set.add({a:1, b:2}.stringifySorted());

set.has({b:2, a:1}.stringifySorted());
// returns true

Get all the values

After we created the set and added the values, we can get all values by

let iterator = set.values();
let done = false;
while (!done) {
  let val = iterator.next();

  if (!done) {
    console.log(val.value);
  }
  done = val.done;
}

Here's a link with all in one file http://tpcg.io/FnJg2i

relief.melone
  • 3,042
  • 1
  • 28
  • 57
  • 1
    "if the keys are sorted" is a big if, especially for complex objects – Alexander Mills Dec 06 '18 at 20:44
  • that's exactly why I chose this approach ;) – relief.melone Sep 23 '19 at 17:42
  • Keep in mind that `JSON.stringify()` has various challenges for a unique and fully descriptive representation of a generic object beyond just key order. It supports only basic types, not things like Set, Map, Symbol or Class instances. It also doesn't handle circular references such as a parent with a reference to its child and the child with a reference to its parent. – jfriend00 Jul 29 '21 at 09:21
4

For Typescript users the answers by others (especially czerny) can be generalized to a nice type-safe and reusable base class:

/**
 * Map that stringifies the key objects in order to leverage
 * the javascript native Map and preserve key uniqueness.
 */
abstract class StringifyingMap<K, V> {
    private map = new Map<string, V>();
    private keyMap = new Map<string, K>();

    has(key: K): boolean {
        let keyString = this.stringifyKey(key);
        return this.map.has(keyString);
    }
    get(key: K): V {
        let keyString = this.stringifyKey(key);
        return this.map.get(keyString);
    }
    set(key: K, value: V): StringifyingMap<K, V> {
        let keyString = this.stringifyKey(key);
        this.map.set(keyString, value);
        this.keyMap.set(keyString, key);
        return this;
    }

    /**
     * Puts new key/value if key is absent.
     * @param key key
     * @param defaultValue default value factory
     */
    putIfAbsent(key: K, defaultValue: () => V): boolean {
        if (!this.has(key)) {
            let value = defaultValue();
            this.set(key, value);
            return true;
        }
        return false;
    }

    keys(): IterableIterator<K> {
        return this.keyMap.values();
    }

    keyList(): K[] {
        return [...this.keys()];
    }

    delete(key: K): boolean {
        let keyString = this.stringifyKey(key);
        let flag = this.map.delete(keyString);
        this.keyMap.delete(keyString);
        return flag;
    }

    clear(): void {
        this.map.clear();
        this.keyMap.clear();
    }

    size(): number {
        return this.map.size;
    }

    /**
     * Turns the `key` object to a primitive `string` for the underlying `Map`
     * @param key key to be stringified
     */
    protected abstract stringifyKey(key: K): string;
}

Example implementation is then this simple: just override the stringifyKey method. In my case I stringify some uri property.

class MyMap extends StringifyingMap<MyKey, MyValue> {
    protected stringifyKey(key: MyKey): string {
        return key.uri.toString();
    }
}

Example usage is then as if this was a regular Map<K, V>.

const key1 = new MyKey(1);
const value1 = new MyValue(1);
const value2 = new MyValue(2);

const myMap = new MyMap();
myMap.set(key1, value1);
myMap.set(key1, value2); // native Map would put another key/value pair

myMap.size(); // returns 1, not 2
Jan Dolejsi
  • 1,389
  • 13
  • 25
0

A good stringification method for the special but frequent case of a TypedArray as Set/Map key is using

const key = String.fromCharCode(...new Uint16Array(myArray.buffer));

It generates the shortest possible unique string that can be easily converted back. However this is not always a valid UTF-16 string for display concerning Low and High Surrogates. Set and Map seem to ignore surrogate validity. As measured in Firefox and Chrome, the spread operator performs slowly. If your myArray has fixed size, it executes faster when you write:

const a = new Uint16Array(myArray.buffer);  // here: myArray = Uint32Array(2) = 8 bytes
const key = String.fromCharCode(a[0],a[1],a[2],a[3]);  // 8 bytes too

Probably the most valuable advantage of this method of key-building: It works for Float32Array and Float64Array without any rounding side-effect. Note that +0 and -0 are then different. Infinities are same. Silent NaNs are same. Signaling NaNs are different depending on their signal (never seen in vanilla JavaScript).

0

As other guys said there is no native method can do it by far. But if you would like to distinguish an array with your custom comparator, you can try to do it with the reduce method.

function distinct(array, equal) {
  // No need to convert it to a Set object since it may give you a wrong signal that the set can work with your objects.
  return array.reduce((p, c) => {
    p.findIndex((element) => equal(element, c)) > -1 || p.push(c);
    return p;
  }, []);
}

// You can call this method like below,
const users = distinct(
    [
      {id: 1, name: "kevin"},
      {id: 2, name: "sean"},
      {id: 1, name: "jerry"}
    ],
    (a, b) => a.id === b.id
);
...
张焱伟
  • 61
  • 7
0

As others have said, there is no way to do it with the current version of Set. My suggestion is to do it using a combination of arrays and maps.

The code snipped below will create a map of unique keys based on your own defined key and then transform that map of unique items into an array.

const array =
  [
    { "name": "Joe", "age": 17 },
    { "name": "Bob", "age": 17 },
    { "name": "Carl", "age": 35 }
  ]

const key = 'age';

const arrayUniqueByKey = [...new Map(array.map(item =>
  [item[key], item])).values()];

console.log(arrayUniqueByKey);

   /*OUTPUT
       [
        { "name": "Bob", "age": 17 },
        { "name": "Carl", "age": 35 }
       ]
   */

 // Note: this will pick the last duplicated item in the list.
-2

To someone who found this question on Google (as me) wanting to get a value of a Map using an object as Key:

Warning: this answer will not work with all objects

var map = new Map<string,string>();

map.set(JSON.stringify({"A":2} /*string of object as key*/), "Worked");

console.log(map.get(JSON.stringify({"A":2}))||"Not worked");

Output:

Worked

Rodrigo João Bertotti
  • 5,179
  • 2
  • 23
  • 34