Comparing Two Sets in JavaScript: Equality, Subset, Superset and Disjoint
JavaScript has isSubsetOf, isSupersetOf and isDisjointFrom but no equals. How to test set equality correctly, what set-like means, and the gotchas that bite.
The 2024 Set methods gave JavaScript real set algebra: union, intersection, difference and symmetricDifference produce new sets, and isSubsetOf, isSupersetOf and isDisjointFrom answer yes-or-no questions about the relationship between two sets. They have been Baseline since June 2024 and are available in every current browser engine and in Node 22 and later.
There is one obvious question the proposal deliberately did not answer: are these two sets equal? There is no Set.prototype.equals. This post covers how to answer it correctly, the three comparison predicates that do exist, and the handful of things about these methods that surprise people the first time.
The three comparison methods you do have
All three return a boolean and none of them mutates anything.
const a = new Set([1, 2, 3]);
const b = new Set([1, 2, 3, 4, 5]);
const c = new Set([9, 10]);
a.isSubsetOf(b); // true - every member of a is in b
b.isSupersetOf(a); // true - the same relation, read the other way
a.isDisjointFrom(c); // true - they share nothing
a.isDisjointFrom(b); // false - they share 1, 2 and 3Two properties of these worth internalising, because they are the source of most confusion:
- Subset is not strict. A set is a subset of itself.
a.isSubsetOf(a)istrue. There is no built-in strict-subset check - you compose one, which we will do below. - The empty set is a subset of everything and is disjoint from everything, including itself.
new Set().isDisjointFrom(new Set())istrue. This is mathematically correct and it will still surprise you in a test at some point.
Set equality: two correct ways
Two sets are equal when each is a subset of the other. That is the definition, and it is also a perfectly good implementation:
const setsEqual = (a, b) => a.isSubsetOf(b) && b.isSubsetOf(a);It is correct and it reads like the maths. It is also doing more work than it needs to, because each call walks a whole set. The faster version uses the fact that for two sets, equal size plus one-way containment is enough:
const setsEqual = (a, b) => a.size === b.size && a.isSubsetOf(b);The size check is an O(1) early exit that rejects most unequal pairs immediately, and when the sizes do match, a one-way subset test is sufficient. This is the version to reach for.
If you are stuck on an older runtime without the Set methods, the hand-rolled equivalent is short:
const setsEqual = (a, b) => {
if (a.size !== b.size) return false;
for (const value of a) {
if (!b.has(value)) return false;
}
return true;
};Strict subset and strict superset
Strict (or proper) subset means every member of a is in b and b has something a does not. Compose it from the size:
const isStrictSubset = (a, b) => a.size < b.size && a.isSubsetOf(b);
const isStrictSuperset = (a, b) => a.size > b.size && a.isSupersetOf(b);
const x = new Set([1, 2]);
const y = new Set([1, 2, 3]);
isStrictSubset(x, y); // true
isStrictSubset(x, x); // falseThe argument has to be set-like, not merely iterable
This is the single most common runtime error with these methods, and it is easy to get wrong because it reads like it should work:
const a = new Set([1, 2, 3]);
a.isSubsetOf([1, 2, 3, 4]);
// TypeError: object is not set-likeEvery one of the new methods requires a set-like argument: an object with a numeric size property, a callable has method, and a callable keys method that returns an iterator. An array has none of those. A plain iterable has none of those.
So arrays need wrapping, and the wrap is cheap enough not to think about:
a.isSubsetOf(new Set([1, 2, 3, 4])); // trueThe pleasant consequence of the set-like contract is that a Map satisfies it - size, has and keys are all there - so you can compare a Set against a Map's keys directly without materialising them:
const required = new Set(["id", "email"]);
const record = new Map([
["id", 7],
["email", "a@b.c"],
["name", "Ada"],
]);
required.isSubsetOf(record); // true - compares against record's keysAny object you write yourself with those three members works too, which is the intended extension point for custom collection types.
How values are compared
Set membership uses SameValueZero, the same algorithm Set and Map have always used. Practically:
NaNequalsNaN, so a set containingNaNbehaves sanely.0and-0are the same member.- Everything else is strict equality, which means objects compare by identity.
That last point is the one that quietly ruins comparisons of sets of objects:
const a = new Set([{ id: 1 }]);
const b = new Set([{ id: 1 }]);
setsEqual(a, b); // false - two distinct object referencesThere is no built-in structural comparison, and adding one is not a small ask - you would need a canonical key per value. The pragmatic approach is to compare sets of primitive keys and keep the objects in a Map beside them:
const byId = new Map(users.map((u) => [u.id, u]));
const incoming = new Set(payload.map((u) => u.id));
const existing = new Set(byId.keys());
const added = incoming.difference(existing);
const removed = existing.difference(incoming);
const unchanged = incoming.intersection(existing);That diff-by-id shape is by far the most common real use of these methods, and it sidesteps identity comparison entirely.
One asymmetry worth knowing
union, intersection and the rest return a new Set whose insertion order follows the receiver first, then the argument. That matters if you iterate the result, and it means a.union(b) and b.union(a) contain the same members in a different order. Set equality, being order-independent, is unaffected - but JSON.stringify([...a]) comparisons are not, which is one more reason not to use stringification as an equality test.
The short version
- Equality:
a.size === b.size && a.isSubsetOf(b). There is no built-inequals. - Strict subset:
a.size < b.size && a.isSubsetOf(b). - Arguments must be set-like (
size,has,keys) - arrays throw,Mapworks. - Membership is SameValueZero, so objects compare by reference. Diff by id instead.
- Baseline since June 2024; Node 22+.
If you want the producing half of this - union, intersection, difference and symmetricDifference with the performance notes - that is in the guide to the JavaScript Set methods.