Set
) ES6
The data structure Set
manages a duplicate-free collection of values and provides fast membership checks and more.
There are three common ways of creating Sets.
First, we can use the constructor without any parameters to create an empty Set:
const emptySet = new Set();
assert.equal(emptySet.size, 0);
Second, we can pass an iterable (e.g., an Array) to the constructor. The iterated values become elements of the new Set:
const set = new Set(['red', 'green', 'blue']);
Third, the .add()
method adds elements to a Set and is chainable:
const set = new Set()
.add('red')
.add('green')
.add('blue');
.add()
adds an element to a Set.
const set = new Set();
set.add('red');
.has()
checks if an element is a member of a Set.
assert.equal(set.has('red'), true);
.delete()
removes an element from a Set.
assert.equal(set.delete('red'), true); // there was a deletion
assert.equal(set.has('red'), false);
.size
contains the number of elements in a Set.
const set = new Set()
.add('foo')
.add('bar');
assert.equal(set.size, 2)
.clear()
removes all elements of a Set.
set.clear();
assert.equal(set.size, 0)
There are four methods for combining two Sets.
set.union(other)
ES2025The result of set.union(other)
is a Set that has the values of both set
and other
:
assert.deepEqual(
new Set(['a', 'b']).union(new Set(['b', 'c'])),
new Set(['a', 'b', 'c'])
);
set.intersection(other)
ES2025The result of set.intersection(other)
is a Set that has the values that set
and other
have in common:
assert.deepEqual(
new Set(['a', 'b']).intersection(new Set(['b', 'c'])),
new Set(['b'])
);
set.difference(other)
ES2025The result of set.difference(other)
is a Set that has the values that are only in set
:
assert.deepEqual(
new Set(['a', 'b']).difference(new Set(['b', 'c'])),
new Set(['a'])
);
Exercise: Implementing
set.union()
, set.intersection()
and set.difference()
exercises/sets/set-union-intersection-difference_test.mjs
set.symmetricDifference(other)
ES2025The result of set.symmetricDifference(other)
is a Set that has the values that are only in set
or only in other
:
assert.deepEqual(
new Set(['a', 'b']).symmetricDifference(new Set(['b', 'c'])),
new Set(['a', 'c'])
);
assert.deepEqual(
new Set(['a', 'b']).symmetricDifference(new Set(['c', 'd'])),
new Set(['a', 'b', 'c', 'd'])
);
What does symmetric difference mean? These are equivalent definitions of the symmetric difference:
set
− other
) ∪ (other
− set
)
set
or only in other
. The formula makes it clear why the symmetric difference is both symmetric and a difference.
set
∪ other
) − (set
∩ other
)
set
and other
– except for the elements that are in both Sets.
set
xor other
set
is inverted. Everything that is outside set
is not changed.
Exercise: Implementing
set.symmetricDifference()
exercises/sets/set-symmetric-difference_test.mjs
There are three methods for checking the relationships between two sets.
set.isSubsetOf(other)
ES2025set.isSubsetOf(other)
returns true
if all elements of set
are in other
:
assert.deepEqual(
new Set(['a', 'b']).isSubsetOf(new Set(['a', 'b', 'c'])),
true
);
set.isSupersetOf(other)
ES2025set.isSupersetOf(other)
returns true
if set
contains all elements of other
:
assert.deepEqual(
new Set(['a', 'b', 'c']).isSupersetOf(new Set(['a', 'b'])),
true
);
set.isDisjointFrom(other)
ES2025set.isDisjointFrom(other)
returns true
if set
and other
have no elements in common:
assert.deepEqual(
new Set(['a', 'b', 'c']).isDisjointFrom(new Set(['x'])),
true
);
Exercises for Set relationship methods
exercises/sets/set-is-equal-to_test.mjs
set.isSubsetOf()
: exercises/sets/is-subset-of_test.mjs
set.isDisjointFrom()
: exercises/sets/is-disjoint-from_test.mjs
A Set method whose parameter is another Set other
does not require other
to be an actual Set; it only has to be Set-like and have the following methods:
interface SetLike<T> {
/** Can be `Infinity` (see next section). */
size: number;
has(key: T): boolean;
/** Returns an iterator for the elements in `this`. */
keys(): Iterator<T>; // only method `.next()` is required
}
Let’s implement a simple Set-like object and use it with methods whose parameters are “other Sets”:
const setLike = {
size: 1,
has(x) { return x === 'b' },
* keys() { yield 'b' },
};
assert.deepEqual(
new Set(['a', 'b', 'c']).difference(setLike),
new Set(['a', 'c']),
);
assert.deepEqual(
new Set(['a', 'b', 'c']).difference(setLike),
new Set(['a', 'c']),
);
assert.equal(
new Set(['a', 'b', 'c']).isSupersetOf(setLike),
true,
);
assert.equal(
new Set(['b']).isSubsetOf(setLike),
true,
);
Maps are also Set-like:
const setLike = new Map([['b', true]]);
assert.deepEqual(
new Set(['a', 'b', 'c']).difference(setLike),
new Set(['a', 'c']),
);
assert.equal(
new Set(['a', 'b', 'c']).isSupersetOf(setLike),
true,
);
The .size
of other
can be Infinity
. That means we can work with infinite Sets:
const evenNumbers = {
has(elem) {
return (elem % 2) === 0;
},
size: Infinity,
keys() {
throw new TypeError();
}
};
assert.deepEqual(
new Set([0, 1, 2, 3]).difference(evenNumbers),
new Set([1, 3])
);
assert.deepEqual(
new Set([0, 1, 2, 3]).intersection(evenNumbers),
new Set([0, 2])
);
This works because these methods only invoke other.keys()
if other.size
is smaller than this.size
.
Only two methods don’t support other
being an infinite Set:
union
symmetricDifference
Why use the interface SetLike
for other
?
other
can be a data structure that is not a Set. It was chosen as a compromise between accepting only Sets and all iterable objects.
Why does JavaScript always enforce the full interface SetLike
for other
and throws an exception if a property is missing or has the wrong dynamic type?
Why was method .keys()
chosen for iterating over elements?
Map
. Two other Set methods would have been nicer but can’t be used with Maps:
.[Symbol.iterator]()
returns key-value pairs.
.values()
is incompatible with the Map method .has()
(which accepts keys, not values).
Source: TC39 proposal
Sets are iterable and the for-of
loop works as we’d expect:
const set = new Set(['red', 'green', 'blue']);
for (const x of set) {
console.log(x);
}
Output:
red
green
blue
As we can see, Sets preserve insertion order. That is, elements are always iterated over in the order in which they were added.
Sets are iterable, which is why we can use Array.from()
to convert a Set to an Array:
const set = new Set(['red', 'green', 'blue']);
assert.deepEqual(
Array.from(set),
['red', 'green', 'blue']
);
We can also perform the conversion via an iterator:
assert.deepEqual(
set.values().toArray(),
['red', 'green', 'blue']
);
set.values()
returns in iterator for the values of set
.
.toArray()
creates an Array with the iterated values.
Sets don’t have a method .map()
. But we can borrow the one that iterators have:
const set = new Set([1, 2, 3]);
const mappedSet = new Set( // (A)
set.values().map(x => x * 2) // (B)
);
assert.deepEqual(
mappedSet,
new Set([2, 4, 6])
);
The previous code shows a common pattern for using iterator methods with Sets:
set.values()
returns an iterator for set
(line B).
.map()
is an iterator method that returns an iterator (line B).
Set
which accepts any iterable as a parameter and uses it to fill the new Set with values.
Filtering Sets works the same way:
const set = new Set([1, 2, 3, 4, 5]);
const filteredSet = new Set(
set.values().filter(x => (x % 2) === 0)
);
assert.deepEqual(
filteredSet,
new Set([2, 4])
);
What if we can’t use iterator methods? Then we can switch to Array methods:
Array.from(set)
set.values()
.
Instead of the Set methods, we can also use iteration to combine Sets:
const a = new Set([1, 2, 3]);
const b = new Set([2, 3, 4]);
// Union
assert.deepEqual(
new Set([...a, ...b]),
new Set([1, 2, 3, 4])
);
// Intersection
assert.deepEqual(
new Set(a.values().filter(x => b.has(x))),
new Set([2, 3])
);
// Difference
assert.deepEqual(
new Set(a.values().filter(x => !b.has(x))),
new Set([1])
);
Grouping via Object.groupBy()
and Map.groupBy()
works for any iterable object and therefore for Sets:
assert.deepEqual(
Object.groupBy(
new Set([0, -5, 3, -4, 8, 9]),
x => Math.sign(x)
),
{
'0': [0],
'-1': [-5,-4],
'1': [3,8,9],
__proto__: null,
}
);
More information: “Grouping iterables ES2024” (§32.8)
Converting an Array to a Set and back, removes duplicates from the Array:
const arr = [1, 2, 1, 2, 3, 3, 3];
const noDuplicates = Array.from(new Set(arr));
assert.deepEqual(
noDuplicates, [1, 2, 3]
);
Strings are iterable and can therefore be used as parameters for new Set()
:
assert.deepEqual(
new Set('abc'),
new Set(['a', 'b', 'c'])
);
As with Map keys, Set elements are compared similarly to ===
, with the exception of NaN
being equal to itself.
> const set = new Set([NaN, NaN, NaN]);
> set.size
1
> set.has(NaN)
true
As with ===
, two different objects are never considered equal (and there is no way to change that at the moment):
> const set = new Set();
> set.add({});
> set.size
1
> set.add({});
> set.size
2
Why do Sets have a .size
, while Arrays have a .length
?
.size
, while Arrays have a .length
?” (§36.7.4).
Why are some method names verbs and others nouns? This is a rough general rule:
this
– e.g.: set.add()
and set.clear()
set.values()
and set.union()
Set
new Set()
new Set(iterable)
ES6
values
, then an empty Set is created.
const set = new Set(['red', 'green', 'blue']);
Set.prototype.*
: working with single Set elementsSet.prototype.add(value)
ES6
value
to this Set.
this
, which means that it can be chained.
const set = new Set(['red']);
set.add('green').add('blue');
assert.deepEqual(
Array.from(set), ['red', 'green', 'blue']
);
Set.prototype.delete(value)
ES6
value
from this Set.
true
if something was deleted and false
, otherwise.
const set = new Set(['red', 'green', 'blue']);
assert.equal(set.delete('red'), true); // there was a deletion
assert.deepEqual(
Array.from(set), ['green', 'blue']
);
Set.prototype.has(value)
ES6
Returns true
if value
is in this Set and false
otherwise.
const set = new Set(['red', 'green']);
assert.equal(set.has('red'), true);
assert.equal(set.has('blue'), false);
Set.prototype.*
: working with all Set elementsget Set.prototype.size
ES6
Returns how many elements there are in this Set.
const set = new Set(['red', 'green', 'blue']);
assert.equal(set.size, 3);
Set.prototype.clear()
ES6
Removes all elements from this Set.
const set = new Set(['red', 'green', 'blue']);
assert.equal(set.size, 3);
set.clear();
assert.equal(set.size, 0);
Set.prototype.*
: iterating and loopingSet.prototype.values()
ES6
Returns an iterable over all elements of this Set.
const set = new Set(['red', 'green']);
for (const x of set.values()) {
console.log(x);
}
Output:
red
green
Set.prototype[Symbol.iterator]()
ES6
Default way of iterating over Sets. Same as .values()
.
const set = new Set(['red', 'green']);
for (const x of set) {
console.log(x);
}
Output:
red
green
Set.prototype.forEach(callback, thisArg?)
ES6
forEach(
callback: (value: T, key: T, theSet: Set<T>) => void,
thisArg?: any
): void
Feeds each element of this Set to callback()
. value
and key
both contain the current element. This redundancy was introduced so that this callback
has the same type signature as the callback
of Map.prototype.forEach()
.
We can specify the this
of callback
via thisArg
. If we omit it, this
is undefined
.
const set = new Set(['red', 'green']);
set.forEach(x => console.log(x));
Output:
red
green
Map
The following methods make the interface of Set
symmetric with the interface of Map
.
Set.prototype.entries(): Iterable<[T,T]>
ES6
Mainly exists so that Sets and Maps have similar interfaces: Each Set element is viewed as a key-value entry whose key and value are both that element:
> new Set(['a', 'b', 'c']).entries().toArray()
[ [ 'a', 'a' ], [ 'b', 'b' ], [ 'c', 'c' ] ]
.entries()
enables us to convert a Set to a Map:
const set = new Set(['a', 'b', 'c']);
const map = new Map(set.entries());
assert.deepEqual(
Array.from(map.entries()),
[['a','a'], ['b','b'], ['c','c']]
);
Set.prototype.keys(): Iterable<T>
ES6
Mainly exists so that Sets and Maps have similar interfaces: Each Set element is viewed as a key-value entry whose key and value are both that element. Therefore the result of .keys()
is the same as the result of .values()
:
> new Set(['a', 'b', 'c']).keys().toArray()
[ 'a', 'b', 'c' ]
Set.prototype.*
: combining two Sets ES2025Set.prototype.union(other)
ES2025
Set<T>.prototype.union(other: SetLike<T>): Set<T>
This method returns a Set that is the union of this
and other
. It contains a value if it is in this
or other
.
assert.deepEqual(
new Set(['a', 'b']).union(new Set(['b', 'c'])),
new Set(['a', 'b', 'c'])
);
other
doesn’t have to be a Set, it only has to be Set-like and have the property .size
and the methods .has(key)
and .keys()
. Sets and Maps both fulfill those requirements.
Set.prototype.intersection(other)
ES2025
Set<T>.prototype.intersection(other: SetLike<T>): Set<T>
This method returns a Set that is the intersection of this
and other
. It contains a value if it is in this
or other
.
assert.deepEqual(
new Set(['a', 'b']).intersection(new Set(['b', 'c'])),
new Set(['b'])
);
other
doesn’t have to be a Set, it only has to be Set-like and have the property .size
and the methods .has(key)
and .keys()
. Sets and Maps both fulfill those requirements.
Set.prototype.difference(other)
ES2025
Set<T>.prototype.difference(other: SetLike<T>): Set<T>
This method returns a Set that is the difference between this
and other
. It contains a value if it is in this
but not in other
.
assert.deepEqual(
new Set(['a', 'b']).difference(new Set(['b', 'c'])),
new Set(['a'])
);
other
doesn’t have to be a Set, it only has to be Set-like and have the property .size
and the methods .has(key)
and .keys()
. Sets and Maps both fulfill those requirements.
Set.prototype.symmetricDifference(other)
ES2025
Set<T>.prototype.symmetricDifference(other: SetLike<T>): Set<T>
This method returns a Set that is the symmetric difference between this
and other
. It contains a value if it is only in this
or only in other
.
assert.deepEqual(
new Set(['a', 'b']).symmetricDifference(new Set(['b', 'c'])),
new Set(['a', 'c'])
);
other
doesn’t have to be a Set, it only has to be Set-like and have the property .size
and the methods .has(key)
and .keys()
. Sets and Maps both fulfill those requirements.
For more information on this method, see its section in this chapter.
Set.prototype.*
: checking Set relationships ES2025Set.prototype.isSubsetOf(other)
ES2025
Set<T>.prototype.isSubsetOf(other: SetLike<T>): boolean
Returns true
if all elements of this
are in other
:
assert.deepEqual(
new Set(['a', 'b']).isSubsetOf(new Set(['a', 'b', 'c'])),
true
);
Set.prototype.isSupersetOf(other)
ES2025
Set<T>.prototype.isSupersetOf(other: SetLike<T>): boolean
Returns true
if this
contains all elements of other
:
assert.deepEqual(
new Set(['a', 'b', 'c']).isSupersetOf(new Set(['a', 'b'])),
true
);
Set.prototype.isDisjointFrom(other)
ES2025
Set<T>.prototype.isDisjointFrom(other: SetLike<T>): boolean
Returns true
if this
and other
have no elements in common:
assert.deepEqual(
new Set(['a', 'b', 'c']).isDisjointFrom(new Set(['x'])),
true
);