JavaScript Set
has long been considered incomplete since its introduction in the ES2015 specification. However, this situation is about to change.
Sets are a collection type that ensures the values within them are unique and non-repeating. In the ES2015 version, the functionality provided by Set
was mainly limited to creating, adding, deleting elements, and checking whether an element belongs to a Set
. If operations or comparisons between multiple sets were needed, custom functions had to be written. Fortunately, the committee responsible for the ECMAScript specification, TC39, along with major browser developers, has made progress in this area. We can now perform operations like union
, intersection
, and difference
in JavaScript.
Before diving into these new features, let's first review what existing JavaScript Sets can do, and then discuss the new Set functions and the JavaScript engines that support these features.
What operations can JavaScript Sets perform in ES2015?#
Exploring the basic functionality of JavaScript Set
through examples is the most straightforward approach.
You can create an empty Set
, or initialize a Set
by providing an iterable object (like an array).
const languages = new Set(["JavaScript", "TypeScript", "HTML", "JavaScript"]);
Since the values in a Set
must be unique, the above Set
actually contains three elements. This can be confirmed using the size
property of the Set
.
languages.size;
// => 3
You can add new elements to the Set
using the add
method. If you try to add an element that already exists, there will be no change.
languages.add("JavaScript");
languages.add("CSS");
languages.size;
// => 4
You can remove elements from the Set
using the delete
method.
languages.delete("TypeScript");
languages.size;
// => 3
You can check if an element belongs to the Set
using the has
method. Compared to arrays, Set
checks are more efficient in this regard, as the time complexity for this operation is constant time (O(1)).
languages.has("JavaScript");
// => true
languages.has("TypeScript");
// => false
You can also iterate over the elements of the Set
using forEach
or for...of
loops. The order of elements is based on the order they were added to the Set
.
languages.forEach(element => console.log(element));
// "JavaScript"
// "HTML"
// "CSS"
Additionally, you can obtain iterators from the Set
using the keys
, values
(which is actually equivalent to keys
), and entries
methods.
Finally, you can use the clear
method to empty a Set.
languages.clear();
languages.size;
// => 0
This is a simple recap of the operations that can be performed with the Set
from the ES2015 specification:
Set
provides methods for handling collections of unique values.- Adding elements to a
Set
and testing for the existence of elements in aSet
is very efficient. - Converting an
Array
or other iterable objects to aSet
is a convenient way to remove duplicates.
However, this implementation lacks the ability to perform operations between Set
s. You might want to merge two Set
s to create a new Set
containing all elements from both (union), find common elements between two Set
s (intersection), or determine elements that are in one Set
but not in another (difference). Until recently, implementing these operations required custom functions.
What do the new Set functions include?#
The proposal for Set
methods adds the following methods to Set
instances: union
, intersection
, difference
, symmetricDifference
, isSubsetOf
, isSupersetOf
, and isDisjointFrom
.
Some of these methods are similar to certain join operations in SQL, and we will demonstrate the function of each through code examples.
The following code examples can be tried in Chrome 122+ or Safari 17+.
Set.prototype.union(other)#
The union of two sets is a set that contains all elements from both sets.
const frontEndLanguages = new Set(["JavaScript", "HTML", "CSS"]);
const backEndLanguages = new Set(["Python", "Java", "JavaScript"]);
const allLanguages = frontEndLanguages.union(backEndLanguages);
// => Set {"JavaScript", "HTML", "CSS", "Python", "Java"}
In this example, all languages from the first and second sets appear in the third set. As with other methods of adding elements to a Set
, duplicate elements are automatically removed.
This is equivalent to performing a SQL FULL OUTER JOIN
on two tables.
Set.prototype.intersection(other)#
The intersection of two sets is a set that contains the common elements from both sets.
const frontEndLanguages = new Set(["JavaScript", "HTML", "CSS"]);
const backEndLanguages = new Set(["Python", "Java", "JavaScript"]);
const frontAndBackEnd = frontEndLanguages.intersection(backEndLanguages);
// => Set {"JavaScript"}
Here, "JavaScript" is the only element that appears in both sets.
The intersection is equivalent to SQL's INNER JOIN
.
Set.prototype.difference(other)#
The difference between the set performing the operation and another set contains all elements unique to the first set.
const frontEndLanguages = new Set(["JavaScript", "HTML", "CSS"]);
const backEndLanguages = new Set(["Python", "Java", "JavaScript"]);
const onlyFrontEnd = frontEndLanguages.difference(backEndLanguages);
// => Set {"HTML", "CSS"}
const onlyBackEnd = backEndLanguages.difference(frontEndLanguages);
// => Set {"Python", "Java"}
When determining the difference between two sets, the order of the sets is very important. In the above example, removing the back-end language set from the front-end language set results in "JavaScript" being removed, leaving "HTML" and "CSS". Conversely, removing the front-end language set from the back-end language set still removes "JavaScript", leaving "Python" and "Java".
The difference is similar to performing a SQL LEFT JOIN
.
Set.prototype.symmetricDifference(other)#
The symmetric difference of two sets is a set that contains all elements unique to both sets.
const frontEndLanguages = new Set(["JavaScript", "HTML", "CSS"]);
const backEndLanguages = new Set(["Python", "Java", "JavaScript"]);
const onlyFrontEnd = frontEndLanguages.symmetricDifference(backEndLanguages);
// => Set {"HTML", "CSS", "Python", "Java"}
const onlyBackEnd = backEndLanguages.symmetricDifference(frontEndLanguages);
// => Set {"Python", "Java", "HTML", "CSS"}
In this case, although the elements in the result set are the same, the order of the elements differs based on which set was called. The order of elements is determined by the order they were added to the set, and the elements of the set performing the operation are added first.
The symmetric difference is similar to SQL's FULL OUTER JOIN
that excludes common elements from both tables.
Set.prototype.isSubsetOf(other)#
If all elements of the first set appear in the second set, then the first set is a subset of the second set.
const frontEndLanguages = new Set(["JavaScript", "HTML", "CSS"]);
const declarativeLanguages = new Set(["HTML", "CSS"]);
declarativeLanguages.isSubsetOf(frontEndLanguages);
// => true
frontEndLanguages.isSubsetOf(declarativeLanguages);
// => false
Any set is a subset of itself.
frontEndLanguages.isSubsetOf(frontEndLanguages);
// => true
Set.prototype.isSupersetOf(other)#
If the first set contains all elements of the second set, then the first set is a superset of the second set. This is the opposite relationship of being a subset.
const frontEndLanguages = new Set(["JavaScript", "HTML", "CSS"]);
const declarativeLanguages = new Set(["HTML", "CSS"]);
declarativeLanguages.isSupersetOf(frontEndLanguages);
// => false
frontEndLanguages.isSupersetOf(declarativeLanguages);
// => true
Any set is a superset of itself.
frontEndLanguages.isSupersetOf(frontEndLanguages);
// => true
Set.prototype.isDisjointFrom(other)#
If two sets have no common elements, then they are disjoint.
const frontEndLanguages = new Set(["JavaScript", "HTML", "CSS"]);
const interpretedLanguages = new Set(["JavaScript", "Ruby", "Python"]);
const compiledLanguages = new Set(["Java", "C++", "TypeScript"]);
interpretedLanguages.isDisjointFrom(compiledLanguages);
// => true
frontEndLanguages.isDisjointFrom(interpretedLanguages);
// => false
In these examples, the sets of interpreted languages and compiled languages have no intersection, so they are disjoint. However, the sets of front-end languages and interpreted languages are not disjoint because they share the element "JavaScript".
Support Status#
As of the writing of this article, these new Set
methods proposal have entered stage 3 of the TC39 standardization process, and Safari 17 (released in September 2023) and Chrome 122 (expected in February 2024) have implemented these methods. Edge will follow closely after Chrome, and Firefox Nightly has also supported these features behind an experimental flag, with both browsers expected to support them officially soon.
Bun also uses Safari's JavaScriptCore engine, so it already supports these new features. Chrome's support for these features means they have been integrated into the V8 JavaScript engine and will soon be adopted by Node.js.
Hopefully, this means these proposals will smoothly transition to stage 4 of the process and may timely become part of the ES2024 specification.
Polyfills#
If you need support for these features on older JavaScript engines, you can use polyfills. These polyfills can be obtained through core-js or provided as separate packages in the es-shims project (for example, the [set.prototype.union package](https://www.npmjs.com/package/set.prototype.union "set.prototype.union package" for implementing union functionality).
If you have already written your own implementations for these features, it is recommended to migrate to these polyfills first, gradually phasing out custom implementations as these features gain widespread support.
The functionality of Sets no longer feels lacking#
JavaScript Set
has long been viewed as incomplete, but the addition of these seven new functions makes its functionality more comprehensive. Building such features into the language itself means we can reduce reliance on external dependencies or custom implementations and focus more on solving real problems.
This is just a part of the many stage 3 proposals currently under consideration by TC39. Check out this list to see what new features might be added to JavaScript next. I am particularly interested in Temporal and Decorators, as these two proposals could change significant parts of how we write JavaScript.