JavaScript Set methods are seven built-in operations on Set.prototype: union(), intersection(), difference() and symmetricDifference() return a new Set, while isSubsetOf(), isSupersetOf() and isDisjointFrom() return a boolean. They are part of ES2025, ship in Chrome 122, Firefox 127, Safari 17 and Node.js 22, and replace the spread-and-filter one-liners many codebases still carry.
If your project still has a utils/sets.ts full of this:
const common = new Set([...a].filter((x) => b.has(x)));
you can delete it. What the method names do not tell you is what the argument may be, what order the result comes back in, and which TypeScript setting hides all seven from your editor.
What do the seven Set methods return?
Four of them build a new Set and leave both inputs untouched. Three answer a yes/no question.
const odds = new Set([1, 3, 5, 7, 9]);
const squares = new Set([1, 4, 9]);
odds.union(squares); // Set(6) { 1, 3, 5, 7, 9, 4 }
odds.intersection(squares); // Set(2) { 1, 9 }
odds.difference(squares); // Set(3) { 3, 5, 7 }
odds.symmetricDifference(squares); // Set(4) { 3, 5, 7, 4 }
new Set([1, 9]).isSubsetOf(odds); // true
odds.isSupersetOf(new Set([1, 9])); // true
odds.isDisjointFrom(new Set([2])); // true
Watch the direction on difference(): a.difference(b) is what is in a and missing from b. Swap them and you get the other half. symmetricDifference() gives you both halves in one Set, which is handy for “did anything change?” but useless for “what was added?”, because it throws away which side each element came from.
Membership uses the same SameValueZero comparison as Set.prototype.has(). Two object literals with identical contents are still two different values, so new Set([{ id: 1 }]).intersection(new Set([{ id: 1 }])) is empty. If you need to compare records, build the Sets from their IDs.
What can you pass as the argument?
Not an array. This is the error people paste into a search box:
TypeError: The .size property is NaN
That is V8’s message when you call mySet.union([1, 2]). The argument has to be a Set or a “set-like” object, which MDN defines as anything with a numeric size property, a has() method and a keys() method that yields the elements. An array has length instead of size, no has(), and its keys() returns indices, so it fails on the first check. Wrap it: mySet.union(new Set(arr)).
A Map does qualify, because map.keys() returns its keys and map.has() tests them. That makes one common job tidy. Say you hold a Set of product IDs from a basket and a Map of products you have already fetched, keyed by ID:
const basketIds = new Set(["p1", "p2", "p3"]);
const cache = new Map([
["p1", { title: "Mug" }],
["p3", { title: "Tote" }],
]);
const toFetch = basketIds.difference(cache); // Set(1) { "p2" }
No [...cache.keys()], no intermediate array. A WeakSet does not qualify; it has no keys().
The receiver is stricter than the argument. this must be a real Set, because the engine reads its internal storage directly rather than calling has() on it. You cannot .call() these methods on a set-like object of your own.
What order do the results come back in?
Sets keep insertion order, and these methods are specified to produce a predictable one, though it differs per method. According to MDN:
union()returns the elements ofthisfirst, followed by elements of the argument that were not already present.difference()keeps the order ofthis.intersection()follows the order of whichever Set is smaller.
That last one surprises people. bigSet.intersection(smallSet) comes back in smallSet’s order, not bigSet’s, because of how the method is implemented (below). If you render the result as a list and the order matters, sort it explicitly rather than relying on which operand happened to be larger today.
Are the Set methods faster than filter and spread?
The algorithmic win is real, and MDN explains where it comes from. intersection() and difference() look at the sizes first. If this has more elements than other.size, they iterate other through keys() and probe this. Otherwise they iterate this and call other.has(). So the cost of intersection() “mostly depends on the size of the smaller set”, in MDN’s words.
The hand-written [...a].filter((x) => b.has(x)) always walks all of a and allocates an array along the way, however small b is. With a 50,000-element a and a 10-element b, the native method does roughly ten lookups where the filter does fifty thousand. We have not benchmarked it, so there is no multiplier here. The native version simply picks the cheaper loop for you.
The subset checks have a similar shortcut. If this.size is larger than other.size, isSubsetOf() cannot be true and does not need to look at any elements.
How do you get TypeScript to see Set methods?
Set your lib (or target) to es2025. Until you do, the compiler reports:
error TS2550: Property 'union' does not exist on type 'Set<number>'. Do you need to change your target library? Try changing the 'lib' compiler option to 'es2025' or later.
TypeScript 6.0 added es2025 as a target and lib value and moved the Set methods, iterator helpers, Promise.try and RegExp.escape out of esnext into it. TypeScript 7.0.2, the current release as of October 2026, ships the same lib.es2025.collection.d.ts. A server-side tsconfig.json for Node 22 or later looks like this:
{
"compilerOptions": {
"target": "es2025",
"lib": ["es2025"],
"module": "nodenext",
"strict": true
}
}
For a browser bundle add "dom" to lib. If you are on TypeScript 5.5 to 5.9, the types live in esnext.collection (included by "lib": ["esnext"]) instead.
One quirk in the typings is worth knowing before it confuses a reviewer. intersection() is declared as returning Set<T & U>, so intersecting a Set<number> with a Set<string> gives you Set<never>. That is correct, since no value is both, but it can look like a bug when one side is a wider union type than you expected. difference() returns Set<T> regardless of the argument’s type, which is what you want.
Which runtimes support them, and do you need a polyfill?
The browser compatibility data lists Chrome and Edge 122, Firefox 127, Safari 17 (including iOS), Node.js 22.0.0, Deno 1.42 and Bun 1.0. MDN marks them Baseline 2024, newly available since June 2024. The TC39 proposal reached stage 4 and the repository was archived in July 2024; the methods are in the ES2025 edition of the spec.
On the server that settles it: if you run Node 22 or 24, call them directly. In the browser, “Baseline 2024” means anyone on an older Safari or a pinned enterprise Chrome will throw TypeError: a.union is not a function. If your analytics show meaningful traffic from browsers older than those versions, core-js 3.50 ships polyfills (core-js/actual/set/union and siblings), and a @babel/preset-env setup with useBuiltIns: "usage" will pull in only the ones you call.
For a library you publish to npm, we would hold off using them in the source until your declared engines floor is Node 22, or ship the polyfill as a peer concern and say so in the README.
Should you switch to Set methods now?
Yes, in Node 22+ services and in any front end whose browser support policy already starts at Baseline 2024. The two changes to make in the same pull request are the tsconfig.json lib bump and a search for new Set([... across the codebase, because most of those lines turn into a single method call. If you are already adopting iterator helpers, the same es2025 lib setting covers both.
Hold back only where you cannot control the runtime: an embeddable widget on third-party sites, or a public package with a Node 20 floor. There, keep the filter-and-spread versions or ship the core-js modules, and revisit when your floor moves. If you are planning that floor move alongside a compiler upgrade, our TypeScript 7 migration notes cover the tsconfig defaults that changed on the way.