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.

returns a new Set
unionintersectiondifferencesymmetricDifference
returns a boolean
isSubsetOfisSupersetOfisDisjointFrom
The seven Set methods, grouped by what they return: a new Set or a boolean.

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.

ABA ∪ B — the whole shaded area
union: both circles are filled. Every element from either set survives, with shared elements counted once.

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.

ABA ∩ B — only the overlap
intersection: only the overlapping lens is filled. An element must be in both sets to survive.

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.”

ABA ∖ B — in A, not in B
difference (A minus B): the left crescent. Elements of A that also live in B are removed.

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).

ABA △ B — in exactly one set
symmetricDifference: both crescents, but not the overlap. It is union minus intersection.

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.

interactiveAll four building methods on two live sets

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).

ABA ⊆ B — A is contained in B (and B is a superset of A)
isSubsetOf: A sits entirely inside B. Every element of A is also in B, so A ⊆ B is true.

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.

ABA ∩ B is empty — the sets are disjoint
isDisjointFrom: the circles never touch. No element belongs to both sets, so the relationship is true.

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.

interactiveThe three yes/no relationships, live

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 size property 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.

interactiveSet-like arguments: Set and Map work, Array throws
receiver (this)
must be a genuine Set instance — the engine reads its internal storage directly
argument (set-like)
.size → a number
.has(v) → boolean
.keys() → iterator
The set-like protocol: the receiver must be a real Set; the argument only needs size, has(), and keys().

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.

How intersection picks the cheaper path
1/4
Variables
small.size=2
big.size=6
small has fewer elements, so it is the one iterated. big is probed with has().

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:

  • NaN equals NaN here, even though NaN === NaN is false. So new Set([NaN]).has(NaN) is true, and NaN participates in unions and intersections like any other value.
  • +0 and -0 are 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

  • Set gained seven algebra methods. Four return a new Setunion, intersection, difference, symmetricDifference — and three return a booleanisSubsetOf, isSupersetOf, isDisjointFrom.
  • None of them mutate the set they’re called on. The originals stay intact.
  • difference is directional (a.difference(b) ≠ b.difference(a)); symmetricDifference, union, and intersection are symmetric in content, though order can differ.
  • The receiver must be a real Set. The argument can be any set-like object — one with size, has(), and keys() — which is why a Map works but a plain Array throws and must be wrapped in a Set.
  • Results have a defined order: union is this then the argument’s new elements; intersection follows the smaller set; difference follows this. intersection also iterates the smaller operand, so its cost scales with that.
  • Comparison uses SameValueZero, so NaN matches NaN, +0 matches -0, and objects compare by reference.
  • Baseline Newly available since June 2024 (Chrome/Edge 122, Firefox 127, Safari 17, Node 22). Polyfill via core-js for older targets.