Sets, For When There Can Only Be One

We live in a world 
that celebrates personality.

Enjoy your uniqueness,
wear it on your sleeve,
wherever you go.

People will love you for it.

        - Lenrolc Srich
        Be yourself

You Are One of a Kind

A Set is a data structure used to represent a distinct collection of items where each item is unique and only appears once. This is such a common need that, if you have been working with JavaScript for a little while, chances are that you have needed to roll your own implementation at some point. Well, you’ll need to do that no more because ES6 comes with a native Set implementation. Hurrah!!

Working With Sets

You can create a new set using the Set constructor:

1 let set = new Set();

Or from an iterable collection like an array:

1 let elementsOfMagic = new Set([
2    'earth', 'fire', 'air', 
3    'earth', 'fire', 'water'
4 ]);
5 
6 console.log(`These are the elements of magic: ` + 
7             `${[...elementsOfMagic]}`);
8 // => These are the elements of magic: earth, fire, air, water

As you can appreciate from the example above, the array had a duplicated value of earth that is removed when creating the Set. That’s because a Set will automatically remove any duplicated items and only store each specific item once.

You can easily add more items to a Set using the add method:

1 elementsOfMagic.add('aether');
2 
3 console.log(`More magic!: ${[...elementsOfMagic]}`);
4 // => More magic!: earth, fire, air, water, aether

The add method is chainable, so adding multiple new items is very convenient:

1 elementsOfMagic.add('earth').add('air').add('water');

You can check whether an item exists within a Set by using the has method:

1 console.log(`Is water one of the sacred elements of magic?` +
2             ` ${elementsOfMagic.has('water')}`)
3 // => Is water one of the sacred elements of magic? true

And you can remove items from a set using delete:

1 elementsOfMagic.delete('aether');
2 
3 console.log(`The aether element flows like the tides and 
4 like the tides sometimes disappears: 
5 ${[...elementsOfMagic]}`);
6 
7 // => The aether element flows 
8 //    like the tides and sometimes disappears: 
9 //    earth,fire,air,water

Additionally, you can get the number of elements within a set using the size property:

1 console.log(`${elementsOfMagic.size} are the elements of magic`);

And remove all the items from a set using clear:

1 const castMagicShield = () => elementsOfMagic.clear();
2 castMagicShield();
3 
4 console.log(`ups! I can't feel the elements: ` + 
5             `${elementsOfMagic.size}`);
6 // => ups! I can't feel the elements: 0

If you take a minute to reflect about the Set API and try to remember the Map from the previous chapter you’ll realize that both APIs are exceptionally consistent with each other. Consistency is awesome, it will help you learn these APIs in a heartbeat and write less error-prone code.

Let’s see how we iterate over the elements of a Set.

Iterating Sets

Just like Map you can iterate over the elements of a Set using the for/of loop:

1 elementsOfMagic.add('fire').add('water').add('air').add('earth');
2 for(let element of elementsOfMagic){
3   console.log(`element: ${element}`);
4 }
5 // => element: fire
6 //    element: water
7 //    element: air
8 //    element: earth

In this case, instead of key/value pairs you iterate over each item within a Set. Notice how the elements are iterated in the same order as they were inserted. The default iterator for a Set is the values iterator. The next snippet of code is equivalent to the one above:

1 for(let element of elementsOfMagic.values()){
2   console.log(`element: ${element}`);
3 }

The Set also has iterators for keys and entries just like the Map although you probably won’t need to use them. The keys iterator is equivalent to values. The entries iterator transforms each item into a key/value pair where both the key and the value are each item in the Set. So if you use the entries iterator you’ll just iterate over [value, value] pairs.

In addition to using either of these iterators, you can take advantage of the Set.prototype.forEach method to traverse the items in a Set:

1 elementsOfMagic.forEach((value, alsoValue, set) => {
2   console.log(`element: ${value}`);
3 })
4 // => element: fire
5 //    element: water
6 //    element: air
7 //    element: earth

Using Array Methods With Sets

The conversion between Sets to Arrays and back is so straightforward that using all the great methods available in the Array.prototype object is one little step away:

1 function filterSet(set, predicate){
2     var filteredItems = [...set].filter(predicate);
3     return new Set(filteredItems);
4 }
5 
6 var aElements = filterSet(elementsOfMagic, e => e.startsWith('a'));
7 console.log(`Elements of Magic starting with a: ${[...aElements]}`);
8 // => Elements of Magic starting with a: air

We saw many of these methods in the Array’s chapter but we will see many more in the functional programming tome where we discover its secret LINQ-like abilities.

How Do Sets Understand Equality?

So far you’ve seen that a Set removes duplicated items whenever we try to add them to the Set. But how does it know whether or not two items are equal?

Well… It uses strict equality comparison (which you may also known as === or !==). This is important to understand because it poses a very big limitation to using Sets in real world applications today. That’s because even though strict equality comparison works great with numbers and strings, it compares objects by reference, that is, two objects are only equal if they are the same object.

Let’s illustrate this problematic situation with an example. Let’s say that we have a Set of persons which, of course, are unique entities (we are all beautiful individual wonders just like precious stones):

1 let persons = new Set();

We create a person object randalf and we attempt to add it twice to the Set:

 1 let randalf = {id: 1, name: 'randalf'};
 2 
 3 persons
 4   .add(randalf)
 5   .add(randalf);
 6 
 7 console.log(`I have ${persons.size} person`)
 8 // => I have 1 person 
 9 
10 console.log([...persons]);
11 // => [[object Object] {
12 //  id: 1,
13 //  name: "randalf"
14 //}]

The Set has our back and only adds the person once. Since it is the same object, using strict equality works in this scenario. However, what would happen if we were to add an object that we considered to be equal in our problem domain?

So let’s say that in a new and innovative view of the world two persons are equal if they have the same properties, and particularly the same id (Imagine randalf meeting randalf from the future, they are equal, but not the same):

 1 persons.add({id: 1, name: 'randalf'});
 2 console.log(`I have ${person.size} persons?!?`)
 3 
 4 // => I have 2 persons?!?
 5 console.log([...persons]);
 6 /*
 7 *= [[object Object] {
 8   id: 1,
 9   name: "randalf"
10 }, [object Object] {
11   id: 1,
12   name: "randalf"
13 }]
14 */

Well, in that case, the object would be added to the Set and as a result, and for all intents and purposes, we would have the same person twice. Unfortunately there’s no way to specify equality for the elements within a Set as of today and we’ll have to wait to see this feature introduced into the language some time in the future.

We are free to imagine how it would look though, and something like this would work wonderfully:

1 let personsSet = new Set([], p => p.id);

In the meantime, if you need to use Set-like functionality for objects your best bet is to use a dictionary indexing objects by a key that represents their uniqueness.

1 var fakeSetThisIsAHack = new Map();
2 fakeSetThisIsAHack
3   .set(randalf.id, randalf)
4   .set(1, {id: 1, name: 'randalf'});
5 console.log(`fake set has ${fakeSetThisIsAHack.size} item`);
6 // => fake set has 1 item

Sets Cheatsheet

Basic Operations

Basics description
var set = new Set() Create an empty set
var set = new Set(iterator) Create a set from an iterator
set.add(‘value’) Add an item to the set if it is not in the set already. The items added to the set can be of any type. It uses strict equality comparison to determine that. Chainable.
set.delete(‘value’) Remove item if it exists. Returns true if an item has been removed and false otherwise.
set.has(‘value’) Check whether an item exists in the set. Returns true or false whether the key exists or not respectively.
set.size Returns the number of items in the set
set.clear() Remove all items within the set

Iterating a Set

Methods of Iteration description
set.forEach((value,value,map) ⇒ {}) Iterate over every item within a set
set.value() Returns a value iterator. This is the Set default iterator
set.keys() Returns a key iterator which just lets you iterate over the items within a set (just like values)
set.entries() Returns a key/value pair iterator which lets you iterate over pairs of value/value for those items within a set

Concluding

The Set is a new data structure in ES6 that lets you easily remove duplicates from a collection of items. It offers a very simple API very consistent with the Map API and it’s going to be a great addition to your arsenal and save you the need to roll your own.

Unfortunately, at present, it has a big limitation that is that it only supports strict equality comparison to determine whether two items are equal. Hopefully in the near future we will be able to define our own custom version of equality and that day Sets will achieve their true potential. Until then use Set with numbers and strings, and rely on Map when you are working with objects.

Exercises