Sets

Collection of unique values

In JavaScript sets are not of an explicit data type. JavaScript provides a predefined Set object. Sets are collections of unique values (both in JavaScript and in a mathematical context). The values can be of any data type.

Sets are different from arrays. Storing a set of elements in a JavaSvaript set has some advantages over storing it in an array, as will be explained in the section Arrays vs. sets.

The number of values can be obtained by the size property, somewhat like the length property of an array.


const mySet = new Set();

mySet.add('How to get this value?');
mySet.add(true);
mySet.add(Symbol('fizz'));
mySet.add({a: 1});
mySet.add({a: 1});

console.log(mySet); // logs: [ "How to get this value?", true, Symbol("fizz"), {a: 1}, {a: 1} ]
console.log(mySet.size); // logs: 5

PS. In the above example it looks like an array is returned to the console. This is how the Firefox browser logs a set in the console. Chrome logs a set between curly brackets: { "How to get this value?", true, Symbol("fizz"), {a: 1}, {a: 1} }.

Each value must be unique in the set's collection. However, multiple values in a set cannot be NaN, even though NaN !== NaN and one value can be 0 while another is -0, even though 0 === -0. In the above example both values {a: 1} reference a different object, so they are considered different values.

Constructor

Sets cannot be created with a literal. They are created with the Set constructor and the new operator. Values are inserted into the set by the add() method.


const mySet = new Set();

const value1 = 'Hello';
const value2 = {};
const value3 = function() {};
const value4 = null;

mySet.add(value1);
mySet.add(value2);
mySet.add(value3);
mySet.add(value4);

console.log(mySet); // logs: Set [ "Hello", {}, value3(), null ]

The Set constructor takes an optional argument. If this argument is an iterable object, all of its elements will be added to the new set. The argument could for instance be an array. This can be used to transform an array into a set, copy sets or merge sets (see later).


const myArray = [1, ["hello", {}], Symbol("foo"), NaN];
const mySet = new Set(myArray);

console.log(mySet); // logs: Set [ 1, [ "hello", {} ], Symbol("foo"), NaN ]

It also provides a way to create and fill a set in a little more compact way than using the Set constructor and the add method.


const mySet = new Set([ 1, true, "hello", {}, null]);
console.log(mySet); // logs: Set [ 1, true, "hello", {}, null ]

Accessing and iterating over values

The set's values are not object properties, like the elements of an array are. Values are not accessible via the regular dot notation or square bracket notation. Enumerating properties, for instance by means of a for...in loop, will not visit the set's values, while it will visit elements in an array.

Since there is no key attached to a set value, you cannot directly get, say, the second value in the set. You can use has() to check if a value is present in the set or not. This does not work if the value is an object, since the has() method's argument references a different object than the key ({} !== {}).


const mySet = new Set([
  {a: 1},
  [2],
]);

console.log(mySet.has({a: 1})); // logs: false
console.log(mySet.has([2])); // logs: false

The set's elements can be iterated over in insertion order, i.e., the order in which each value was first inserted into the set. In a for...of loop the set's built-in iterator returns, at every iteration, the subsequent value. Also Set.prototype.forEach() can be used to iterate over the set's elements.


const mySet = new Set([
  "pit",
  "pot",
  "put"
]);

mySet.add("pot".replace(/o/, "e"));

console.log(mySet.size); // logs: 4
console.log(mySet.has('pet')); // logs: true

mySet.prop = "someValue";
console.log(mySet.prop); // logs: "someValue"
console.log(Object.getOwnPropertyNames(mySet)); // logs: Array [ "prop" ]
console.log(mySet.size); // logs: 4

const values = [];
for (const element of mySet) {
  values.push(element);
}
console.log(values); // logs: Array [ "pit", "pot", "put", "pet" ]

values.length = 0;
mySet.forEach(element => values.push(element));
console.log(values); // logs: Array [ "pit", "pot", "put", "pet" ]

Like Array and Map also Set has methods values() and keys(). But since sets do not involve keys, Set.prototype.values() and Set.prototype.keys() do exactly the the same: they return an iterator/iterable that yields the value for each element in the set in insertion order. Looping over this iterator/iterable by a for...of loop will yield the same result as for...of looping over the set itself.


const mySet = new Set([
  "pit",
  "pot",
  "put",
  "pet"
]);

const valuesArray = [],
      keys = mySet.keys(),
      values = mySet.values();
	  
for (const element of keys) {
  valuesArray.push(element);
}
console.log(valuesArray); // logs: Array [ "pit", "pot", "put", "pet" ]
valuesArray.length = 0;

for (const element of values) {
  valuesArray.push(element);
}
console.log(valuesArray); // logs: Array [ "pit", "pot", "put", "pet" ]

// ***********

for (const element of values) {
  console.log(element);
}
// logs nothing; an object that is both an iterator and an iterable
// can only be iterated over once.

BTW. An iterable iterator with Symbol.iterator returning the iterable itself, can only be iterated over once.

Deleting set elements

The Set.prototype.delete() method can be used to delete a value from the set.

The size property cannot be used to truncate a set, unlike the length property on an array. Method clear can be used to delete all values from the set.


const mySet = new Set([
  "Hello",
  313,
  true,
  null
]);

console.log(mySet); // logs: Set [ "Hello", 313, true, null ]
mySet.delete(true);
console.log(mySet.has(true)); //logs: false

mySet.size = 0;
console.log(mySet.size); // logs: 3

mySet.clear();
console.log(mySet.size); // logs: 0

Cloning and merging sets

We have seen that we can use the Set constructor taking an iterable object as the argument. We can use this to clone or merge sets. The clones and merges are shallow, meaning the data itself is not cloned.

You can directly use the original set as argument, but you can also use spread syntax in an array as the argument. In the latter case, sets essentially convert to arrays first. This may be a little unwieldy for just cloning a set, but useful to merge sets, as will be explained next.


const originalSet = new Set([ 2, 3, 5, 7 ]);

const cloneSet = new Set(originalSet);
console.log(cloneSet); // logs: Set [ 2, 3, 5, 7 ]
console.log(originalSet === cloneSet); // false

const otherCloneSet = new Set([...originalSet]);
console.log(otherCloneSet); // logs: Set [ 2, 3, 5, 7 ]

Sets can be merged using an array argument for the constructor. This way you can merge any iterable objects to a set. The value uniqueness will be maintained by overwriting a previous value with the last iterated over same value.


const firstSet = new Set([ 2, 3, 5, 7, 11 ]);
const secondSet = new Set([ 11, 13, 17, 19 ]);

const mergedSet = new Set([...firstSet, ...secondSet]);
console.log(mergedSet); // logs: Set [ 2, 3, 5, 7, 11, 13, 17, 19 ]

const myString = "QED"; // the String wrapper object is iterable
const composedSet = new Set([...mergedSet, 23, 29, ...myString]);
console.log(composedSet); // logs: Set [ 2, 3, 5, 7, 11, 13, 17, 19, 23, 29, "Q", "E", "D" ]

Converting to and from sets

We have seen that we can use the Map constructor to convert iterable objects to sets.


const myArray = [ 2, 4, 6, 8 ];
const mySet = new Set(myArray);

console.log(mySet); // logs: Set [ 2, 4, 6, 8 ]

We can convert sets into arrays by using Array.from() or spread syntax. Note that all conversions are shallow.


const mySet = new Set([ 2, 4, 6, 8 ]);

console.log(Array.from(mySet)); // logs: Array [ 2, 4, 6, 8 ]

console.log([...mySet]); // logs: Array [ 2, 4, 6, 8 ]

Strings can be converted to sets via the iterable String wrapper object. Conversion is case sensitive and duplicate characters are omitted.


const myString = "Oompa Loompa";
const mySet = new Set(myString);
console.log(mySet); // logs: Set [ "O", "o", "m", "p", "a", " ", "L" ]

Converting the set back to a string:


const mySet = new Set([ "O", "o", "m", "p", "a", " ", "L" ]);

console.log(Array.from(mySet).join("")); // logs: "Oompa L"
console.log([...mySet].join("")); // logs: "Oompa L"

We can use array-set-array conversion to remove duplicate elements from an array.


const myArray = [ "a", "b", "c", "c", "d", "e", "e", "f", "F" ];
const purgedArray = [...new Set(myArray)];
console.log(purgedArray); // logs: Array [ "a", "b", "c", "d", "e", "f", "F" ]

BTW. Note that the uniqueness of string values in a set is case sensitive.

Mathematical set operations

In mathematics, set theory features binary operations on sets. Some of them are:

Set A is a superset of set B if all elements of B are also elements of A. Set B is then a subset of A.


function isSuperset(set, subset) {
  for (const elem of subset) {
    if (!set.has(elem)) {
      return false;
    }
  }
  return true;
};
console.log(
  isSuperset(
    new Set(["pit", "pet", "pot", "put"]),
	new Set(["pet", "pot"])
  )
);
// logs: true

Arrays vs. sets

Storing a set of elements in a JavaScript set has some advantages over storing it in an array.