skip to content

How do you compute the union, intersection, and difference of two JavaScript Sets, both with the built-in Set methods and by hand?

level: middleimportance: nice to knowfreq 34%

answer

  1. seven methods, one release
  2. new Set out, operands untouched
  3. the argument must be set-like
  4. arrays throw, wrap them first
  5. spread plus filter is the fallback

basics

~10 s

Modern engines provide Set.prototype.union, intersection, difference and symmetricDifference, which return a new Set without mutating either operand. Before those existed the idioms were new Set([...a, ...b]) for union and [...a].filter(x => b.has(x)) for intersection.

solid answer

~40 s

Since ES2025 a Set carries the operations directly: `a.union(b)`, `a.intersection(b)`, `a.difference(b)` and `a.symmetricDifference(b)` all return a **new** Set and leave both operands untouched, alongside the predicates `isSubsetOf`, `isSupersetOf` and `isDisjointFrom`. The argument must be *set-like* — an object with a numeric `size`, a `has` method and a `keys` method — so another `Set` or a `Map` works but a plain array throws a `TypeError`. Where you cannot rely on that baseline, the hand-rolled versions are one-liners: union is `new Set([...a, ...b])`, intersection is `new Set([...a].filter(x => b.has(x)))`, and difference is the same filter with `!b.has(x)`. Each of those relies on the Set being iterable and on `has` doing a SameValueZero lookup.

code

javascript · 12 lines
javascript
const a = new Set([1, 2, 3]);
const b = new Set([3, 4]);

// ES2025 built-ins (non-mutating)
console.log([...a.union(b)]);               // [1, 2, 3, 4]
console.log([...a.intersection(b)]);        // [3]
console.log([...a.difference(b)]);          // [1, 2]
console.log([...a.symmetricDifference(b)]); // [1, 2, 4]

// Portable equivalents
const intersection = (x, y) => new Set([...x].filter(v => y.has(v)));
console.log([...intersection(a, b)]);       // [3]

go deeper

for a junior

Know that a Set can be combined with another Set, and be able to write the spread-and-filter one-liners for union and intersection without looking them up.

for a middle

Name the built-in methods, state that they return a new Set rather than mutating, and explain the set-like argument requirement that makes passing an array throw.

for a senior

Show you check the runtime baseline before relying on ES2025 methods, and that you project domain objects to identity keys before intersecting, since raw object references never match across two payloads.

for a principal

Decide where set arithmetic belongs at all — a client-side diff of two payloads is often a symptom of an API that should return the delta itself, and that tradeoff is yours to name.

## The built-in methods Set operations were added to the language in ES2025 and shipped across engines during 2023–2024 (Safari 17, Chrome 122, Firefox 127, Node 22). Seven methods arrived together: ```js const a = new Set([1, 2, 3]); const b = new Set([3, 4]); a.union(b); // Set { 1, 2, 3, 4 } a.intersection(b); // Set { 3 } a.difference(b); // Set { 1, 2 } a.symmetricDifference(b); // Set { 1, 2, 4 } a.isSubsetOf(b); // false a.isSupersetOf(new Set([1])); // true a.isDisjointFrom(new Set([9])); // true ``` Two properties matter more than the names. First, **the four combining methods return a brand-new Set and mutate nothing** — `a` and `b` are unchanged, which makes them safe to chain. Second, **the three predicates return booleans**, not Sets. ## The set-like requirement The argument is not required to be a `Set`, but it must be *set-like*: an object exposing a numeric `size` property, a callable `has`, and a callable `keys` that returns an iterator. This is the detail interviewers probe, because the obvious mistake fails loudly: ```js new Set([1, 2]).union([2, 3]); // TypeError — an array has no size property new Set([1, 2]).union(new Set([2, 3])); // fine ``` A `Map` happens to satisfy the protocol, and since the methods read `keys()`, `someSet.intersection(someMap)` operates on the Map's **keys**. If you have an array, wrap it: `a.union(new Set(arr))`. The receiver, by contrast, must be a real `Set` — these are `Set.prototype` methods that touch internal slots, so calling them on a set-like object throws. ## Doing it by hand The manual versions predate the methods and still appear constantly in code that targets older baselines, in interview whiteboards, and wherever the input is an array rather than a Set: ```js const union = (a, b) => new Set([...a, ...b]); const intersection = (a, b) => new Set([...a].filter(x => b.has(x))); const difference = (a, b) => new Set([...a].filter(x => !b.has(x))); const symmetricDifference = (a, b) => new Set([...difference(a, b), ...difference(b, a)]); ``` They work because a Set is iterable — spread drains its values in insertion order — and because `has` performs the same SameValueZero lookup the built-ins use. Note that `difference` is **asymmetric**: `difference(a, b)` is what is in `a` and not in `b`, which is not the same as `difference(b, a)`. Candidates routinely conflate that with symmetric difference, which is the union of both one-sided differences. The predicates are equally short by hand: ```js const isSubsetOf = (a, b) => [...a].every(x => b.has(x)); const isDisjointFrom = (a, b) => ![...a].some(x => b.has(x)); ``` ## Ordering and identity The results are Sets, so they iterate in the order their values were inserted while being built; `union` naturally lists the receiver's values first, then whatever the other operand contributes that was not already there. Do not build logic on a precise ordering of `intersection` — treat the result as an unordered collection and sort explicitly if presentation order matters. Membership across all of these is the Set's own equality: `NaN` matches `NaN`, `+0` and `-0` are one value, and objects match only by reference. So intersecting two arrays of freshly-parsed objects yields an empty Set no matter how similar the contents look; map to identity keys first. ## Choosing between them When your runtime baseline includes the methods, prefer them: they say what they mean, avoid building intermediate arrays, and let the engine iterate the smaller collection where that is legal. Reach for the hand-rolled versions when you must support an older baseline, when your inputs are arrays you would otherwise have to wrap anyway, or when the "set" is really a domain concept — dedupe by id, diff two API payloads — where you want the filter predicate to be something richer than raw value equality.

  • Do these methods mutate the Set they are called on?
    No. `union`, `intersection`, `difference` and `symmetricDifference` all build and return a new `Set`, leaving both the receiver and the argument untouched — which is what makes `a.difference(b).union(c)` safe to chain. If you want in-place behaviour you have to write it yourself, for example iterating `b` and calling `a.delete(x)` for a destructive difference.
  • What is the difference between difference and symmetricDifference?
    `a.difference(b)` is one-sided: the values in `a` that are not in `b`, so it is not commutative — `a.difference(b)` and `b.difference(a)` generally differ. `a.symmetricDifference(b)` is the union of both one-sided differences: everything in exactly one of the two Sets, and it *is* commutative. Confusing the two is the classic mistake when diffing an old and a new payload.
  • How would you intersect two arrays of objects by a business id rather than by reference?
    Project to identities first: build `const ids = new Set(b.map(o => o.id))`, then `a.filter(o => ids.has(o.id))`. Intersecting the object arrays directly returns nothing, because Set membership matches objects by reference and each parsed record is a fresh object. Keeping the Set full of primitive ids also makes the lookup exact and the code obvious about which field defines identity.

saying these in an interview costs you the question

  • Passes a plain array to union and expects it to work
  • Says the methods mutate the receiving Set
  • Treats difference as commutative like symmetric difference
  • Expects intersection of look-alike objects to be non-empty
  • Assumes the methods exist on every runtime baseline

context