ECMAScript proposal: Set methods

[2022-12-14] dev, javascript, es proposal
(Ad, please don’t block)

In this blog post, we examine the ECMAScript proposal “Set methods for JavaScript” by Michał Wadas, Sathya Gunasekara and Kevin Gibbons. It introduces new methods for Sets.

New Set methods that return Sets  

Set.prototype.union(other)  

This method returns a Set that is the union of this and other.

Type signature:

Set<T>.prototype.union(other: SetReadOperations<T>): Set<T>

The type of other, SetReadOperations is discussed later. It means that other provides all operations that the new methods need for their algorithms.

Example:

assert.deepEqual(
  new Set(['a', 'b', 'c']).union(new Set(['c', 'd'])),
  new Set(['a', 'b', 'c', 'd'])
);

Set.prototype.intersection(other)  

This method returns a Set that is the intersection of this and other.

Type signature:

Set<T>.prototype.intersection(other: SetReadOperations<T>): Set<T>

Example:

assert.deepEqual(
  new Set(['a', 'b', 'c']).intersection(new Set(['c', 'd'])),
  new Set(['c'])
);

Set.prototype.difference(other)  

This method returns a Set that is the difference between this and other.

Type signature:

Set<T>.prototype.difference(other: SetReadOperations<T>): Set<T>

Example:

assert.deepEqual(
  new Set(['a', 'b', 'c']).difference(new Set(['c', 'd'])),
  new Set(['a', 'b'])
);

Set.prototype.symmetricDifference(other)  

This method returns a Set that is the symmetric difference between this and other. What does that mean? These are equivalent definitions of the symmetric difference:

  • thisotherotherthis
  • (thisother) − (thisother)
  • this xor other (exclusive OR)
  • All elements that only exist in one of the two sets

Type signature:

Set<T>.prototype.symmetricDifference(other: SetReadOperations<T>): Set<T>

Example:

assert.deepEqual(
  new Set(['a', 'b', 'c']).symmetricDifference(new Set(['c', 'd'])),
  new Set(['a', 'b', 'd'])
);
assert.deepEqual(
  new Set(['a', 'b']).symmetricDifference(new Set(['c', 'd'])),
  new Set(['a', 'c', 'b', 'd'])
);

New Set methods that return booleans  

Set.prototype.isSubsetOf(other)  

This method returns true if this is a subset of other and false otherwise.

Type signature:

Set<T>.prototype.isSubsetOf(other: SetReadOperations<T>): boolean

Example:

assert.deepEqual(
  new Set(['a', 'b', 'c']).isSubsetOf(new Set(['a', 'b'])),
  false
);
assert.deepEqual(
  new Set(['a', 'b']).isSubsetOf(new Set(['a', 'b', 'c'])),
  true
);

Set.prototype.isSupersetOf(other)  

This method returns true if this is a superset of other and false otherwise.

Type signature:

Set<T>.prototype.isSupersetOf(other: SetReadOperations<T>): boolean

Example:

assert.deepEqual(
  new Set(['a', 'b', 'c']).isSupersetOf(new Set(['a', 'b'])),
  true
);
assert.deepEqual(
  new Set(['a', 'b']).isSupersetOf(new Set(['a', 'b', 'c'])),
  false
);

Set.prototype.isDisjointFrom(other)  

This method returns true if this is disjoint from other and false otherwise.

Type signature:

Set<T>.prototype.isDisjointFrom(other: SetReadOperations<T>): boolean

Example:

assert.deepEqual(
  new Set(['a', 'b', 'c']).isDisjointFrom(new Set(['c', 'd'])),
  false
);
assert.deepEqual(
  new Set(['a', 'b', 'c']).isDisjointFrom(new Set(['x'])),
  true
);

Rules for this and other  

For all of the new Set methods:

  • this must be an instance of Set.
  • other must implement the interface SetReadOperations shown below.
    • The full interface is always enforced, even if a method doesn’t use all of its methods.
interface SetReadOperations<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
}

Infinite Set-like data  

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])
);

Only two methods don’t support other being an infinite Set:

  • union
  • symmetricDifference

The rationales behind the API design  

These are the rationales behind the API design (source):

  • Why does this have to be a Set?

    • TC39 could have chosen a more flexible interface for this which would have enabled us to use the Set methods generically. However, not doing so makes implementations simpler and faster.
  • Why use an interface for other?

    • Due to the interface, other can be a data structure other than a Set. It was chosen as a compromise between accepting only Sets and any iterable objects.
  • Why is the full interface always enforced for other?

    • That makes the API simpler and hides implementation details.
  • Why was the method name .keys() chosen for iterating over data structure elements?

    • That’s due to compatibility with the only Set-like data structure currently in the standard library – Map:
      • The method key Symbol.iterator doesn’t work because that Map method returns key-value pairs.
      • The method key 'values' doesn’t work because that Map method is not compatible with the Map method .has() (which accepts keys, not values).
  • Why are the new method names nouns and not verbs like .add()?

    • A rough general rule (with exceptions) is that verb methods mutate this, while noun methods return new data – for example: set.add() and set.keys().

Implementations  

I’m aware of the following two polyfills:

Further reading