ODINODIN

Worth Knowing About Set in JavaScript

2019-10-09

frontend

A Set is a fundamental data structure that is useful in many situations. It is simply a collection of unique elements, for some definition of uniqueness (which we'll come back to). In ES2015, JavaScript gained support for Sets, and in line with the language's tradition, the implementation has some sharp edges worth knowing about.

API

Here's how to use a Set in JavaScript.

// Create
const animals = new Set(['cat', 'dog']) 

// Add an element
animals.add('giraffe') 
// Result: Set(3) {'cat', 'dog', 'giraffe'}

// Remove an element
animals.delete('cat')
// Result: true

// Size
animals.length
// Result: undefined

animals.size
// Result: 2

// Check membership
animals.has('dog')
// Result: true

Set keeps track of insertion order, so when you iterate, you get the content back in the same order you put it in.

for (let d of animals) console.log(d);
// Logs 'dog' then 'giraffe'

Equality, in JavaScript's Eyes

No surprises so far. A Set supports arbitrary types, so we can put anything in a Set. Numbers, strings, objects, lists, and even other Sets. This looks promising!

const fruit = new Set()

const apple = {name: 'apple'}

fruit.add(apple)
// Result: Set(1)

fruit.add(apple)
// Result: Set(1)

Then we can check if there are apples in the fruit

fruit.has({name: 'apple'})
// Result: false

Doh! But unfortunately, exactly as expected. It's consistent with the concept of equality in JavaScript. The comparison mechanism is about checking references, not values.

This means:

// We use the reference to apple
fruit.has(apple)
// Result: true

// Two different objects with the same values
const elements = new Set([{name: 'Thorium'}, {name: 'Thorium'} ])
// Result: Set(2)

Working with values instead of references is more intuitive and leaves less room for errors. If you want value semantics, you need to resort to libraries like Immutable.js or languages like ClojureScript.

Functional Programming

We all love functional programming. Arrays have supported map, filter, and reduce for a long time. The use of Set also fits nicely into that paradigm.

elements.map(f => f.name)
// Error: elements.map is not a function 

Surprisingly, Set doesn't support map, filter, or reduce. There is, however, a proposal to add it.

In the meantime, you need to convert to an Array first.

[...elements].map(f => f.name) 
// Result: ['Thorium', 'Thorium']

Set, not F-Venn-dly

If there's one thing a Set can do better than any other data structure, it's performing mathematical Set operations like union, intersection, and disjunctions. Hold on tight.

const presidents = new Set(['Abraham', 'Bill', 'Donald'])
const cartoonCharacters = new Set(['Donald', 'Langbein', 'Svampebob'])

// Which presidents are also cartoon characters?
presidents.intersection(cartoonCharacters)
// Error: presidents.intersection is not a function

🤯🤯🤯🤯🤯

This was a big surprise. There was probably a good reason it wasn't supported out of the box. Once again, there is a proposal to add Set functions to the language. So at least we have something to look forward to.

While we wait, we can create our own Set functions, as outlined in this Mozilla article

function intersection(setA, setB) {
    var _intersection = new Set();
    for (var elem of setB) {
        if (setA.has(elem)) {
            _intersection.add(elem);
        }
    }
    return _intersection;
}

Set is Data, Right?

Let's try to share information about a Set with others

JSON.stringify({uniqueNames: new Set(['monkey', 'cat'])})
// Result: "{"uniqueNames":{}}"

That's not so strange, since the JSON standard is based on a subset of JavaScript and is not extensible.

So if you want to share Sets, you need to come up with your own encoding or convert them to an Array.

If we could dream of a better world, it would be nice if Set got its own syntax literal.

const minArray = [1,2,3]
const mittSet = #{1,2,3} // <-- This is not valid JavaScript syntax, just a wish!

When is it Appropriate to Use Set?

Despite a somewhat deficient implementation, Set is useful to have in your toolkit.

For data types that are actually compared based on values, such as numbers and strings, you can easily ensure that a collection contains only unique values.

const uniqueNames = new Set([])
uniqueNames.add('Janne')
uniqueNames.add('Janne')

With an array or an object, you would have to work a bit harder for the same effect.

Another example is where you have a collection of objects and want to keep track of a subset of them.

const animals = [{id: '1', name: 'Cat', age: 4}, 
             {id: '2', name: 'Dog', age: 5},
             {id: '3', name: 'Horse', age: 8}]

const selected = new Set(['1', '2'])

Beyond that, we'll have to cross our fingers that Set support gets better in the future.