JavaScript Set
has long been regarded as 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 such as 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 in ES2015 perform?#
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 by 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 whether an element belongs to the Set
using the has
method. Compared to arrays, Set
is more efficient in this regard, as this operation has a constant time complexity (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 clear a Set using the clear
method.
languages.clear();
languages.size;
// => 0
This is a simple review of the operations that can be performed using 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
are 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 Sets
. You might want to merge two Sets
to create a new Set
containing all elements from both (union), find common elements between two Sets
(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 purpose of each function 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. Like other methods for 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 languages from the front-end languages results in "JavaScript" being removed, leaving "HTML" and "CSS". Conversely, removing the front-end languages from the back-end languages 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 the calling set. 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
excluding 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, 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, with Safari 17 (released in September 2023) and Chrome 122 (expected in February 2024) implementing these methods. Edge followed closely behind Chrome, while Firefox Nightly has experimental support for these features, and both browsers are 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.
This hopefully means that 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 support for these features becomes widespread.
The functionality of Sets no longer feels lacking#
JavaScript Set
has long been seen as functionally incomplete, but the addition of these seven new functions makes it much more comprehensive. Building such functionality 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 may be added to JavaScript next. I am particularly interested in Temporal and Decorators, as these two proposals could change significant aspects of how we write JavaScript.