jsguides

Set.prototype.intersection()

intersection(other)

Syntax

Set.prototype.intersection() is called on a Set instance and returns a new Set containing every element that appears in both the receiver and a given set-like object:

setInstance.intersection(other)

Parameters

  • other — A Set instance, or any set-like object exposing a has method and a size property. Arrays do not qualify on their own; wrap them in new Set(arr) first.

Return value

A new Set containing the elements that are present in both this and other. Neither input is modified.

Description

Set.prototype.intersection() is the ES2025 equivalent of mathematical set intersection: A ∩ B = {x ∈ A | x ∈ B}. It is one of seven new Set methods added in the 16th edition of ECMA-262, alongside union, difference, symmetricDifference, isSubsetOf, isSupersetOf, and isDisjointFrom. Membership uses SameValueZero, the same equality Set uses internally, so NaN equals NaN and +0 equals -0. The method ships in Baseline 2024 and is part of the new Set methods proposal by Michał Wadas.

Two details follow from how the engine reads the receiver’s internal storage:

  • The receiver must be a real Set. Calling intersection() on a plain set-like throws a TypeError.
  • The argument is set-like-friendly. Any object with has and size works.

The implementation picks the cheaper side. When this is larger than other, the engine iterates other and probes this for membership. Otherwise it walks this once and calls other.has(x) for each element. The result therefore follows the iteration order of the smaller input, not the receiver, which is a real gotcha if you care about order:

const big = new Set([1, 2, 3, 4, 5, 6, 7, 8, 9, 10]);
const small = new Set([10, 9, 8, 7, 6]);

console.log(big.intersection(small));
// Set(5) { 10, 9, 8, 7, 6 }

intersection is not variadic. To compute the intersection of three or more sets, chain the calls. Each intermediate set is a fresh allocation, so the cost scales with the number of operands:

const devs = new Set(["Alice", "Bob", "Carol"]);
const reviewers = new Set(["Bob", "Carol", "Dan"]);
const admins = new Set(["Carol", "Eve"]);

console.log(devs.intersection(reviewers).intersection(admins));
// Set(1) { 'Carol' }

The chain reads left-to-right, narrowing the candidate set with each step. Because every call returns a fresh Set, none of the inputs are touched, and intermediate results are safe to store in a const. The downside is allocation: intersecting N sets allocates N-1 intermediate sets on top of the final one, so for long pipelines you may want to fold the inputs into one larger set first and only intersect the dimension that actually filters the data.

Examples

Find elements in both sets

The common case is two real Set instances. The result preserves the iteration order of whichever input is smaller, so check both directions if order matters to downstream code that walks the set:

const odds = new Set([1, 3, 5, 7, 9]);
const squares = new Set([1, 4, 9]);

console.log(odds.intersection(squares));
// Set(2) { 1, 9 }

Pass a set-like argument

Any object that exposes has and size works as the second argument, which is useful when integrating with libraries that already maintain a custom collection:

const setLike = {
  size: 2,
  has(value) { return value === "y" || value === "z"; },
  keys() { return ["y", "z"].values(); },
};

console.log(new Set(["x", "y", "z"]).intersection(setLike));
// Set(2) { 'y', 'z' }

keys() is required only when the set-like ends up being the smaller side of the comparison. The spec walks it to enumerate candidates, so without keys() the call throws. To be defensive, always define keys() on a set-like you pass to intersection, even when you are sure it will be the larger side. A future refactor that flips the operands is otherwise a footgun.

NaN is treated as equal to itself

SameValueZero equality means NaN matches NaN, so a NaN on the right keeps the NaN on the left. This differs from === but matches the behavior of Set membership and Array.prototype.includes:

console.log(new Set([NaN, 1, 2]).intersection(new Set([NaN])));
// Set(1) { NaN }

The receiver must be a real Set

Set.prototype.intersection reads the receiver’s internal [[SetData]] slot directly, so plain objects that only look like a set throw a brand-check error. This is the same guard the spec applies to every built-in Set.prototype method, including the older has, add, and delete, so a duck-typed shim is not interchangeable with a real Set:

const fakeSet = {
  size: 2,
  has: () => true,
  keys: function* () { yield 1; yield 2; },
};

try {
  fakeSet.intersection(new Set([1, 2, 3]));
} catch (err) {
  console.log(err.constructor.name);
  // TypeError
}

If you need to compute an intersection against a custom collection, flip the operands: realSet.intersection(fakeSet). The real Set becomes the receiver, the brand check passes, and the set-like only has to satisfy the has / size / keys contract on the argument side.

Inputs are not mutated

intersection() always allocates a fresh Set. Calling it has no side effects on either input, which is what makes the chain-friendly style above safe to write inside reducers, render functions, or other spots where you would otherwise have to clone first to avoid leaking state. The original sets keep their size, their iteration order, and every element they held before the call:

const left = new Set([1, 2, 3]);
const right = new Set([2, 3, 4]);
const shared = left.intersection(right);

console.log(shared); // Set(2) { 2, 3 }
console.log(left);   // Set(3) { 1, 2, 3 }
console.log(right);  // Set(3) { 2, 3, 4 }

Set.prototype.intersection() performance

Before ES2025, the idiomatic way to compute set intersection in JavaScript was to spread one of the sets into an array and filter by membership against the other:

const a = new Set([1, 2, 3, 4, 5, 6, 7, 8, 9, 10]);
const b = new Set([2, 4, 6, 8, 10]);

const result = new Set([...a].filter(x => b.has(x)));
console.log(result);
// Set(5) { 1, 3, 5, 7, 9 }

Set.prototype.intersection() does the same work without materialising an intermediate array, and it routes through whichever of the two sets is smaller. The asymptotic cost is O(min(n, m)) when both inputs are real Set instances, because the has() probes against the larger side are amortized O(1). Arrays ruin that gain, since Array.prototype.includes is linear, so when one side is genuinely an array the filter version is comparable in cost and sometimes clearer. For very large sets in hot paths, the savings show up as fewer allocations and better cache behaviour, since the engine never builds the throwaway array the spread-and-filter pattern produces.

Specifications

Set.prototype.intersection() is defined in ECMA-262 § 24.2.4.3, part of the 16th edition (ECMAScript 2025). The spec text is short and worth reading: it states that the receiver is a real Set instance, picks the smaller of the two collections to iterate, and uses SameValueZero equality for the membership test. The full proposal text and meeting notes live in the tc39/proposal-new-set-methods repository.

Browser support

Set.prototype.intersection() is part of the new Set methods proposal by Michał Wadas, which graduated to Stage 4 in 2023 and ships with the ECMAScript 2025 specification. Support reached Baseline 2024 in June 2024:

EngineFirst version
Chrome / Edge122 (Feb 2024)
Firefox127 (Jun 2024)
Safari17.0 (Sep 2023)
Node.js22.0.0

For older environments, both core-js and es-shims provide a polyfill. core-js ships a broad collection-level patch, while es-shims offers a smaller, focused polyfill that covers the new Set methods individually and follows the spec closely.

See also