12k
All articles

A Practical Guide to JavaScript's New Set Methods

JavaScript Set methods explained: union, intersection, difference, symmetricDifference, and subset checks, plus Set-like Map support and browser support.

OpenReplay Team
OpenReplay Team
A Practical Guide to JavaScript's New Set Methods

JavaScript’s Set object gained seven new instance methods — union(), intersection(), difference(), symmetricDifference(), isSubsetOf(), isSupersetOf(), and isDisjointFrom() — that became Baseline Newly Available as of June 11, 2024. These methods replace the Array.filter() + Array.includes() patterns frontend developers have hand-rolled for years to compare collections, and they do so with set-based lookups instead of nested array scans. If you already know the ES2015 Set API — add, has, delete, iteration — this guide gives you the parts the announcement posts skipped: the Set-like argument protocol that lets you pass a Map, the reference-equality trap that silently breaks object comparisons, copy-pasteable frontend patterns, and an honest browser-support table.

Key Takeaways

  • All seven new Set methods are Baseline Newly Available as of June 11, 2024, shipping in Chrome 122, Edge 122, Firefox 127, and Safari 17, plus Node.js 22.0.0 — no polyfill is required for current targets.
  • The argument to these methods does not need to be a Set instance; it must expose a numeric size property, a callable has method, and a callable keys method, which means a Map works directly.
  • Set compares values by reference, so new Set([{id:1}]).intersection(new Set([{id:1}])) returns an empty set — intersect Sets of stable primitive IDs instead.
  • difference() is not commutative, and symmetricDifference() returns receiver-only elements first, in receiver order.
  • Replacing [...a].filter(x => b.includes(x)) with a.intersection(b) swaps an O(n²) array pattern for one that scales with the smaller set under the spec’s average sublinear Set access.

The seven methods at a glance

The new methods split into two groups: four that return a new Set and three that return a boolean. This split is the most useful mental model — it tells you immediately whether you’re building a collection or asking a yes/no question.

Operations that return a new Set:

Predicates that return a boolean:

The proposal that added these is TC39’s Set methods proposal, which reached Stage 4, is now inactive, was added to the ECMAScript specification, and is part of ECMAScript 2025.

Using the set-returning methods in real frontend code

The four set-returning methods — union(), intersection(), difference(), and symmetricDifference() — each return a new Set without mutating either input, and each maps cleanly onto a task frontend developers already do by hand.

union(): merge feature-flag overrides with defaults

union() returns a new set containing every element from both sets, with duplicates removed.

const defaults = new Set(["new-dashboard", "dark-mode"]);
const overrides = new Set(["dark-mode", "beta-search"]);

const enabled = defaults.union(overrides);
// Set(3) { "new-dashboard", "dark-mode", "beta-search" }

The classic version of this was new Set([...defaults, ...overrides]). union() expresses the intent directly. When you merge a base set of enabled feature flags with per-user or per-environment overrides, union() gives you the effective flag set in one call.

intersection(): find which routes a user can actually access

intersection() returns a new set containing only the elements present in both sets.

const requiredForRoute = new Set<string>(["billing:read", "billing:write"]);
const userPermissions = new Set<string>(["billing:read", "users:read"]);

const satisfied = requiredForRoute.intersection(userPermissions);
// Set(1) { "billing:read" }

For route access control, intersect the permission set a route requires against the permission set a user holds. The size of the result against the required set tells you whether access is partial or complete.

difference(): diff previous-vs-next selection in a multi-select

difference() returns a new set containing elements in this set but not in the other — and it is not commutative.

const prevSelected = new Set<string>(["a", "b", "c"]);
const nextSelected = new Set<string>(["b", "c", "d"]);

const added = nextSelected.difference(prevSelected);   // Set(1) { "d" }
const removed = prevSelected.difference(nextSelected); // Set(1) { "a" }

For multi-select components, nextSelected.difference(prevSelected) gives you the items just added, and prevSelected.difference(nextSelected) gives you the items just removed — two set operations replacing a pattern that required sorting or nested loops. a.difference(b) returns elements in a absent from b, while b.difference(a) returns the inverse; argument order is the operation.

symmetricDifference(): highlight which keys changed between two snapshots

symmetricDifference() returns a new set containing elements in either set but not in both.

const before = new Set<string>(["name", "email", "phone"]);
const after = new Set<string>(["name", "email", "address"]);

const changedKeys = before.symmetricDifference(after);
// Set(2) { "phone", "address" }

To highlight which keys appeared or disappeared between two snapshots of a state object, take the symmetric difference of their key sets. One detail the announcement posts gloss over: iteration order depends on the receiver. Per ECMA-262 2025, symmetricDifference() returns the receiver-only elements in the receiver’s order, followed by the other-only elements in other.keys() order. The set of elements is identical regardless of which side you call it on; the order is not.

Using the boolean predicates for authorization checks

The three predicate methods — isSubsetOf(), isSupersetOf(), and isDisjointFrom() — each return a boolean, and each maps onto a common authorization or input-validation check.

isSupersetOf(): check for all required scopes

isSupersetOf() returns true if this set contains every element of the given set.

const grantedScopes = new Set<string>(["read", "write", "delete"]);
const requiredScopes = new Set<string>(["read", "write"]);

const hasAllRequiredScopes = grantedScopes.isSupersetOf(requiredScopes);
// true

To check whether a user’s granted OAuth scopes cover all required scopes for an operation, grantedScopes.isSupersetOf(requiredScopes) returns true in a single call — equivalent to [...requiredScopes].every(s => grantedScopes.has(s)) but expressed as a set relationship.

isSubsetOf(): verify a tag list is fully supported

isSubsetOf() returns true if every element of this set is in the given set.

const supportedTags = new Set<string>(["sale", "new", "featured", "clearance"]);
const requestedTags = new Set<string>(["sale", "new"]);

const allSupported = requestedTags.isSubsetOf(supportedTags);
// true

When a caller passes a list of tags or filters, requestedTags.isSubsetOf(supportedTags) confirms every one is recognized before you run the query.

isDisjointFrom(): detect conflicting modifier keys

isDisjointFrom() returns true if the two sets share no elements.

const pressedKeys = new Set<string>(["Meta", "Shift"]);
const conflictingModifiers = new Set<string>(["Control", "Alt"]);

const noConflict = pressedKeys.isDisjointFrom(conflictingModifiers);
// true

For keyboard-shortcut handling, isDisjointFrom() lets you assert that no conflicting modifier keys are held before you fire an action.

The Set-like protocol: the argument is not a Set

The argument to any of these methods does not need to be a Set instance — it must be an object with a numeric size property, a callable has method, and a callable keys method. A Map satisfies this requirement natively, which means mySet.intersection(myMap) is valid and checks against the map’s keys. Every announcement post describes the argument as “another set,” which is technically incomplete.

This protocol is defined in the ECMA-262 specification under GetSetRecord, and MDN documents the Set-like object requirement directly.

const map = new Map([
  ["a", 1],
  ["b", 2],
]);
const set = new Set(["a", "c"]);

set.intersection(map);
// Set(1) { "a" }  — checks against the map's keys

Confirmed in Node v22.16.0, set.intersection(map) returns Set { 'a' } because "a" is the only element of the set that also appears among the map’s keys. This matters for interop: you can intersect a Set against the keys of a Map without building an intermediate new Set(map.keys()), and any immutable-collection library or custom index object that exposes size, has, and keys plugs into these methods without conversion.

The object identity gotcha

Set compares values by reference, not by structure. MDN’s value-equality documentation confirms this. The practical consequence is a trap when your set holds objects:

new Set([{ id: 1 }]).intersection(new Set([{ id: 1 }]));
// Set(0) {}

Reference equality is the gotcha: this returns an empty set because the two {id: 1} objects are distinct references, even though they look identical. The fix is to intersect Sets of stable primitive IDs, then re-hydrate from a lookup map:

type User = { id: number; name: string };

const prev: User[] = [{ id: 1, name: "Ada" }, { id: 2, name: "Lin" }];
const next: User[] = [{ id: 2, name: "Lin" }, { id: 3, name: "Mo" }];

const byId = new Map(next.map((u) => [u.id, u]));

const prevIds = new Set(prev.map((u) => u.id));
const nextIds = new Set(next.map((u) => u.id));

const addedIds = nextIds.difference(prevIds); // Set(1) { 3 }
const added = [...addedIds].map((id) => byId.get(id)!);
// [{ id: 3, name: "Mo" }]

Reference-equality bugs like this one leave no stack trace. The set operation completes without error, returns an empty set, and the UI simply does not update — a multi-select that won’t change, a permission badge that won’t clear, a diff view that shows no changes. Because there’s no exception, it doesn’t surface in error monitoring. Session replay is a technique that makes this class of silent-failure bug visible: you watch the user interact, the operation complete, and the interface respond to nothing.

Performance: why this beats filter + includes

Replacing [...a].filter(x => b.includes(x)) with a.intersection(b) is not just a readability win. Array.includes() is O(n), making the filter pattern O(n²) over two n-element collections, while intersection() scales with the smaller of the two sets, assuming the average sublinear Set access the specification requires. The ECMA-262 Set objects section mandates that Set access perform sublinearly on average — it does not guarantee O(1) — and the MDN intersection() reference describes the smaller-set scaling behavior.

OperationArray patternComplexityNative Set methodComplexity
Intersection[...a].filter(x => b.includes(x))O(n²)a.intersection(b)scales with smaller set, avg. sublinear access
Unionnew Set([...a, ...b])O(n + m)a.union(b)O(n + m)
Difference[...a].filter(x => !b.includes(x))O(n²)a.difference(b)scales with a, avg. sublinear access

The gap widens with collection size: at a few elements it’s irrelevant, but the array patterns degrade quadratically while the native methods do not.

Migration recipes

Map your existing array-comparison code onto the new methods directly. These three substitutions cover the bulk of real-world cases.

Old patternNew method
[...a].filter(x => b.includes(x))a.intersection(b)
[...new Set([...a, ...b])]a.union(b) (returns a Set)
[...a].filter(x => !b.includes(x))a.difference(b)

If your data lives in arrays, wrap each side in new Set(...) once, run the operation, and spread back to an array if a downstream API needs one:

const a = ["x", "y", "z"];
const b = ["y", "z", "w"];

const common = [...new Set(a).intersection(new Set(b))];
// ["y", "z"]

The conversion to Set is itself O(n), but the round trip avoids the nested scan required by the O(n²) filter + includes pattern and scales better as collections grow.

Browser support and polyfills

All seven Set methods are Baseline Newly Available as of June 11, 2024, shipping in Chrome 122 (Feb 20, 2024), Edge 122 (Feb 23, 2024), Firefox 127 (Jun 11, 2024), and Safari 17 (Sep 18, 2023), with no polyfill required for current browser targets. They also ship in Node.js 22.0.0 (Apr 24, 2024) via V8 12.4.

EnvironmentVersionRelease date
Chrome122Feb 20, 2024
Edge122Feb 23, 2024
Firefox127Jun 11, 2024
Safari17Sep 18, 2023
Node.js22.0.0 (V8 12.4)Apr 24, 2024

For older targets, the core-js library and the es-shims project provide spec-compliant polyfills. If you support only current evergreen browsers and Node.js 22+, you can drop the polyfill entirely.

Where to go next

Audit your codebase for filter + includes and filter + !includes over deduplicated data — those are the direct candidates for intersection() and difference(). Before you migrate object-holding collections, switch them to Sets of stable primitive IDs to avoid the reference-equality trap, and remember that any Map, immutable collection, or custom index exposing size, has, and keys can be passed as the argument directly. The methods are Baseline, the semantics are spec-stable, and the array patterns they replace were never as cheap as they looked.

FAQs

Can I pass a Map directly to Set.intersection() or do I need to convert it first?

You can pass a Map directly. The new Set methods accept any Set-like object, defined in the ECMA-262 specification as an object with a numeric size property, a callable has method, and a callable keys method. A Map satisfies this natively, so mySet.intersection(myMap) checks against the map's keys without building an intermediate new Set(map.keys()). Immutable-collection library sets and custom index objects exposing the same three members also work without conversion.

Why does Set intersection return an empty set when both sets contain identical objects?

Because Set compares values by reference, not by structure. The expression new Set([{id:1}]).intersection(new Set([{id:1}])) returns an empty set since the two {id:1} objects are distinct references even though they look identical, confirmed in Node v22.16.0. The fix is to build Sets of stable primitive IDs, run the operation on those, then re-hydrate the matched objects from a lookup Map keyed by ID.

What is the difference between difference() and symmetricDifference()?

difference() is directional and not commutative: a.difference(b) returns elements in a that are absent from b, while b.difference(a) returns the inverse. symmetricDifference() returns elements in either set but not both, and it is order-independent in content. Their iteration orders differ too: symmetricDifference() returns receiver-only elements in the receiver's order, followed by other-only elements in other.keys() order, so the set of results is identical regardless of which side you call but the ordering is not.

Do I still need a polyfill for the new Set methods in production?

Not for current browser targets. All seven methods are Baseline Newly Available as of June 11, 2024, shipping in Chrome 122, Edge 122, Firefox 127, Safari 17, and Node.js 22.0.0 via V8 12.4. If you support only current evergreen browsers and Node.js 22 or later, you can drop the polyfill entirely. For older targets, the core-js library and the es-shims project provide spec-compliant polyfills you can include selectively.

Open-source session replay

Complete picture for complete understanding

Capture every clue your frontend is leaving so you can instantly get to the root cause of any issue with OpenReplay — the open-source session replay tool for developers. Self-host it in minutes, and have complete control over your customer data.

Star on GitHub12k

We use cookies to improve your experience. By using our site, you accept cookies.