Set Algebra Is Now a Language Feature
JavaScript Set finally ships union, intersection, difference and three predicate methods. The set-like protocol, the ordering rules, and the reference-equality trap.
For years, comparing two collections in JavaScript meant hand-drawing a Venn diagram in a filter callback. If you wanted the overlap between a user's permissions and a route's requirements, you wrote [...required].filter(p => granted.has(p)) and hoped you remembered which side was which. The result was correct, but the intent was buried in loop mechanics, and difference versus symmetricDifference was exactly the kind of thing a reviewer would get backwards.
That genre of code is over. Set now ships the algebra directly.[1]
The seven methods, and which are directional
Four methods return a new Set: union(), intersection(), difference(), and symmetricDifference().[1] Three return a boolean: isSubsetOf(), isSupersetOf(), and isDisjointFrom().[1] None of them mutate either input.[11]
The directional ones are where bugs used to live. difference() returns elements in the receiver that are not in the argument, and MDN states plainly that the order of elements in the returned set is the same as in this.[3] So granted.difference(required) and required.difference(granted) are different questions, and the method names now make you say which one you mean at the call site instead of inside a predicate.[3]
The predicate methods are less obvious than they look. isSubsetOf() is not a proper-subset test: the subset relationship is not proper subset, which means isSubsetOf() returns true if this and other contain the same elements.[6] The same note applies to isSupersetOf().[7] A set is therefore a subset of itself, so an access check that needs "has at least one extra permission" cannot be expressed with isSubsetOf() alone — you need a size comparison as well.[6]
The set-like protocol is the real API design decision
Here is the part that surprises people in review. The argument to these methods does not have to be a Set — but it cannot be a plain array. All set composition methods require this to be an actual Set instance, but their arguments just need to be set-like, which MDN defines as an object providing a numeric size property, a has() method, and a keys() method.[1]
That third requirement is the one that trips people up. The set-like protocol invokes the keys() method instead of [Symbol.iterator]() to produce elements, specifically so that maps are valid set-like objects: for maps, the iterator produces entries but the has() method takes keys.[1] The practical consequence is that a Map works directly as the argument, and it is treated as the set of its keys.[1] You can intersect a Set of active session IDs against a Map keyed by user ID without materialising new Set(map.keys()) first.[1]
Arrays fail all three tests — no has(), no size, and keys() yields indices rather than elements — as do WeakSets, which have no keys() method at all.[1] That is not a wart; it is the API refusing to let you pay for an O(n) membership scan on every probe. Requiring has() is what makes the fast path possible, and MDN notes that the has method is on average faster than Array.prototype.includes when an array has a length equal to a set's size.[1]
Ordering guarantees you can actually rely on
union() returns elements in this first, followed by elements from other, per MDN.[4] symmetricDifference() follows the same receiver-first rule.[5] difference() preserves the receiver's order.[3]
intersection() is the one with a wrinkle, and it is worth internalising because it is observable. If there are more elements in this than other.size, the engine iterates over other and keeps the elements present in this; otherwise it iterates this and probes other.has(e).[2] Because of this implementation, the efficiency of intersection() mostly depends on the size of the smaller set between this and other, and the order of elements in the returned set is the same as that of the smaller of this and other.[2]
Two takeaways. First, intersection is sublinear in the larger set, which is the algorithmic reason to prefer it over the filter/includes idiom — that older pattern is quadratic over two n-element collections.[2] Second, do not write tests that assert on intersection's ordering unless you know which input is smaller; the order is a function of relative sizes, not of the receiver.[2]
The reference-equality trap
Set has always compared object values by identity. Value equality is based on the SameValueZero algorithm, and for object keys equality is based on object identity — they are compared by reference, not by value.[1]
Set algebra makes this trap more likely to bite, not less, because set operations look like data operations.[1] Intersecting two sets of { id, name } objects from separate API responses returns an empty set, silently.[1] The fix is to run the algebra on stable primitive IDs and re-hydrate afterwards. That is a discipline, not a library feature, and no amount of method sugar removes it.
TypeScript support arrived in two stages
TypeScript 5.5 declares the new proposed methods for the ECMAScript Set type — the four set-returning methods and the three boolean predicates — and none of them mutate the original sets.[11] The declarations for Set, ReadonlySet, and ReadonlySetLike were contributed by Kevin Gibbons.[11]
The lib story then moved on. TypeScript 6.0 adds support for the es2025 option for both target and lib; while there are no new JavaScript language features in ES2025, the new target adds types for built-in APIs and moves declarations from esnext into es2025 — including Promise.try, iterator methods, and the Set methods.[12]
If you are on TypeScript 5.5 through 5.x and still targeting esnext, you already have the types.[11] If your lib is pinned to something older, the methods exist at runtime but the compiler will reject them, which is the worst of both worlds: a red squiggle over code that works in every browser you support.[12]
Runtime support
The methods reached Baseline Newly Available on June 11, 2024, when the feature became available in all three major browser engines.[9] The version numbers: Chrome 122, Firefox 127, and Safari 17, with Node.js 22.0.0, according to MDN's browser-compat-data.[13] The TC39 proposal repository is at stage 4 and archived — the methods have been added to the specification, and the repository is no longer active.[10]
That combination — Baseline since mid-2024, spec-complete, and typed since TypeScript 5.5 — means the honest question is no longer "is this safe to use" but "why is this still hand-rolled in our codebase."[9][11]
Where it earns its keep
The places I reach for it: route guards that intersect a user's permission set against a route's requirements; feature-flag resolution that unions a default set with per-environment overrides; diffing a previous selection against a current one with symmetricDifference() to find what changed in one call; and isDisjointFrom() as a cheap early exit before doing expensive work on two collections that cannot possibly overlap.[8]
Each of those replaces a four-line spread-and-filter expression with one named operation, and the name carries the direction. That is the actual win.[1]
Sources
[1] https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Set — MDN: Set
[2] https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Set/intersection — MDN: Set.prototype.intersection()
[3] https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Set/difference — MDN: Set.prototype.difference()
[4] https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Set/union — MDN: Set.prototype.union()
[5] https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Set/symmetricDifference — MDN: Set.prototype.symmetricDifference()
[6] https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Set/isSubsetOf — MDN: Set.prototype.isSubsetOf()
[7] https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Set/isSupersetOf — MDN: Set.prototype.isSupersetOf()
[8] https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Set/isDisjointFrom — MDN: Set.prototype.isDisjointFrom()
[9] https://web.dev/blog/set-methods — web.dev: The JavaScript Set methods are now part of Baseline
[10] https://github.com/tc39/proposal-set-methods — TC39 proposal: Set Methods for JavaScript
[11] https://www.typescriptlang.org/docs/handbook/release-notes/typescript-5-5.html — TypeScript 5.5 release notes
[12] https://www.typescriptlang.org/docs/handbook/release-notes/typescript-6-0.html — TypeScript 6.0 release notes
[13] https://github.com/mdn/browser-compat-data — MDN browser-compat-data: Set