Set Methods
For years, asking “which tags do these two posts share?” meant writing a loop, or reaching for a utility library. The Set type stored unique values and could tell you whether it held one thing, and that was about it. If you wanted the overlap between two sets, or everything in one that wasn’t in the other, you built it yourself.
That gap is closed. Modern JavaScript gives Set seven methods that implement the classic algebra of sets — the operations you drew as overlapping circles in a maths class. Four of them hand you a brand-new Set; three answer a yes/no question about how two sets relate.
None of these methods touch the set you call them on. union and friends return a fresh Set; the originals are left exactly as they were. That immutability is the whole reason they’re pleasant to chain and reason about.
If you need a refresher on Set itself — how it stores unique values and remembers insertion order — that’s covered in Map and Set. This article assumes you know new Set(), .add(), .has(), and .size, and focuses on the algebra.
The four that build a new Set
Two example sets carry us through all four. Think of them as tags on two blog posts.
const postA = new Set(["js", "css", "html", "webgl"]);
const postB = new Set(["css", "html", "react", "node"]);
union — everything, deduplicated
a.union(b) returns a new set containing every element that appears in a or b. Duplicates collapse, because it’s still a set.
postA.union(postB);
// Set(6) { "js", "css", "html", "webgl", "react", "node" }
The order is defined: first all of a’s elements in their existing order, then any of b’s elements that weren’t already there. So "js", "css", "html", "webgl" come first, then "react" and "node" follow. "css" and "html" appear once, in their a positions.
intersection — only what’s shared
a.intersection(b) returns the elements present in both sets.
postA.intersection(postB);
// Set(2) { "css", "html" }
This is the “what do they have in common?” query. Shared tags, mutual friends, permissions a user holds that a route requires — all intersections.
difference — mine, minus yours
a.difference(b) returns the elements in a that are not in b. Order follows a.
postA.difference(postB);
// Set(2) { "js", "webgl" }
postB.difference(postA);
// Set(2) { "react", "node" }
Note the two lines give different answers. difference is directional — a.difference(b) is not b.difference(a). Read a.difference(b) as “everything in a, take away anything that’s also in b.”
symmetricDifference — the exclusive-or
a.symmetricDifference(b) returns everything that’s in exactly one of the two sets — never both. It’s the union minus the intersection.
postA.symmetricDifference(postB);
// Set(4) { "js", "webgl", "react", "node" }
The shared tags "css" and "html" drop out; each unique tag from either side stays. Order here is a’s unique elements first, then b’s unique elements. Unlike plain difference, this one is symmetric: swapping the operands gives the same set (possibly in a different order).
Rather than trust the descriptions, edit the two tag lists below and watch all four operations recompute at once. Notice how difference changes when you swap which set has the extra tags, while symmetricDifference stays the same either way.
The three that answer yes or no
The remaining methods don’t build a collection; they report a relationship as true or false.
isSubsetOf and isSupersetOf
a.isSubsetOf(b) is true when every element of a is also in b. a.isSupersetOf(b) is the mirror: true when a contains everything in b. They’re two views of the same containment.
const roles = new Set(["read", "write"]);
const granted = new Set(["read", "write", "delete", "admin"]);
roles.isSubsetOf(granted); // true — user has every role we require
granted.isSupersetOf(roles); // true — same fact, said the other way
granted.isSubsetOf(roles); // false — granted has extras
This is a clean way to express permission checks: “does the user hold at least these capabilities?” is required.isSubsetOf(userHas).
A set is always a subset of itself, and the empty set is a subset of everything — the same edge cases you’d get from the mathematical definition.
isDisjointFrom
a.isDisjointFrom(b) is true when the two sets share no elements at all — their intersection is empty.
const weekend = new Set(["sat", "sun"]);
const weekdays = new Set(["mon", "tue", "wed", "thu", "fri"]);
weekend.isDisjointFrom(weekdays); // true — no day is in both
It’s the fast, intention-revealing way to ask “do these two collections overlap?” without materialising the intersection just to check its size.
These three questions are easiest to feel by driving them. Edit the two sets and watch each relationship flip. Try making A a strict subset of B, then add one tag to A that B lacks, then make the two lists share nothing at all.
The argument can be any “set-like” object
Here’s the detail that trips people up. The set you call the method on must be a real Set. But the argument you pass can be any set-like object. Set-like means it provides three things:
- a
sizeproperty holding a number, - a
has(value)method returning a boolean, - a
keys()method returning an iterator of the elements.
Because a Map has all three (its keys() yields keys, has checks keys, size is the count), you can pass a Map straight in — it behaves like the set of its keys.
const tags = new Set(["css", "js", "react"]);
const scores = new Map([
["css", 10],
["react", 7],
["vue", 3],
]);
tags.intersection(scores);
// Set(2) { "css", "react" } — keys shared between the two
If the argument is missing any of the three requirements — size isn’t a number, has or keys isn’t callable — the method throws a TypeError before doing any work. This strictness is deliberate: the methods read the argument through that protocol, so it has to be complete.
The demo below runs the same intersection call against three different arguments: a real Set, a Map (set-like, so it works on its keys), and a plain Array (not set-like — it throws). Press each button to see which succeed and which raise a TypeError.
Ordering, and a subtle performance twist
Every returned set has a defined element order, so results are reproducible across engines:
- union — all of
this, then the new elements of the argument. - intersection — follows whichever of the two sets is smaller.
- difference — follows
this. - symmetricDifference — the unique elements of
this, then the unique elements of the argument.
That “smaller set” wording in intersection isn’t just about ordering — it hints at how the operation runs. To compute the overlap, the engine iterates the smaller collection and checks each element against the larger one. Which side gets iterated (via keys()) versus probed (via has()) depends on the relative sizes, so the work scales with the smaller set, not the larger.
Membership uses SameValueZero
Set membership — and therefore all of these methods — compares values with the SameValueZero rule, the same one Set has always used. Two consequences worth remembering:
NaNequalsNaNhere, even thoughNaN === NaNisfalse. Sonew Set([NaN]).has(NaN)istrue, andNaNparticipates in unions and intersections like any other value.+0and-0are treated as the same value.
Objects still compare by reference. Two structurally identical objects are different elements, so union, intersection, and the rest operate on identity, not deep equality.
const a = { id: 1 };
new Set([a]).intersection(new Set([{ id: 1 }]));
// Set(0) {} — different object references, no overlap
Replacing the old workarounds
Before these landed, every codebase grew its own versions. The native methods are shorter, faster (they touch the smaller set and skip building intermediate arrays), and immutable by default.
// intersection, the old way
const shared = new Set([...a].filter((x) => b.has(x)));
// now
const shared = a.intersection(b);
// union, the old way
const all = new Set([...a, ...b]);
// now
const all = a.union(b);
// difference, the old way
const only = new Set([...a].filter((x) => !b.has(x)));
// now
const only = a.difference(b);
If you were pulling in a utility library purely for _.intersection, _.union, or _.difference over arrays, you can often drop the dependency: wrap your arrays in Sets, run the native method, and spread the result back to an array if you need one.
const arrA = ["a", "b", "c"];
const arrB = ["b", "c", "d"];
const merged = [...new Set(arrA).union(new Set(arrB))];
// ["a", "b", "c", "d"]
Availability
These seven methods reached Baseline Newly available on 11 June 2024, meaning they work across the current versions of every major browser engine. Concretely: Chrome and Edge 122+, Firefox 127+, and Safari 17+. On the server, Node.js has shipped them since version 22.
By mid-2026 that support is broad and stable, so for evergreen browsers and current Node you can use them directly. If you still target much older runtimes — legacy Safari, Node 20 or earlier — you’ll want a polyfill (core-js implements all seven) or a transpiler shim. The methods are pure library additions with no new syntax, so a polyfill is a clean, complete fix rather than a partial one.
Summary
Setgained seven algebra methods. Four return a new Set —union,intersection,difference,symmetricDifference— and three return a boolean —isSubsetOf,isSupersetOf,isDisjointFrom.- None of them mutate the set they’re called on. The originals stay intact.
differenceis directional (a.difference(b) ≠ b.difference(a));symmetricDifference,union, andintersectionare symmetric in content, though order can differ.- The receiver must be a real
Set. The argument can be any set-like object — one withsize,has(), andkeys()— which is why aMapworks but a plain Array throws and must be wrapped in aSet. - Results have a defined order:
unionisthisthen the argument’s new elements;intersectionfollows the smaller set;differencefollowsthis.intersectionalso iterates the smaller operand, so its cost scales with that. - Comparison uses SameValueZero, so
NaNmatchesNaN,+0matches-0, and objects compare by reference. - Baseline Newly available since June 2024 (Chrome/Edge 122, Firefox 127, Safari 17, Node 22). Polyfill via
core-jsfor older targets.