loke.dev
Header image for A Quiet Evolution of the JavaScript Set

A Quiet Evolution of the JavaScript Set

Native collection arithmetic has finally arrived in the browser, rendering dozens of common utility functions and manual filter loops obsolete.

· 4 min read

A Quiet Evolution of the JavaScript Set

JavaScript's Set object has been the underachiever of the standard library for nearly a decade. Since ES6, it was essentially a glorified array that promised "no duplicates" but offered almost nothing else—no math, no logic, and certainly no grace. If you wanted to see if two sets overlapped, you were stuck writing manual loops or pulling in a heavy utility library just to feel like a "real" developer.

But the wait is over. Without much fanfare, the major browsers have shipped Set Composition. We finally have native methods for collection arithmetic—union, intersection, difference, and more. It’s time to delete those utils/setHelpers.ts files and embrace the spec.

The "Utility Tax" We Used to Pay

Before these updates, finding the common elements between two sets (an intersection) looked like a leetcode warmup exercise. You probably wrote something like this more times than you'd care to admit:

const setA = new Set(['apples', 'oranges', 'bananas']);
const setB = new Set(['bananas', 'grapes', 'apples']);

// The "old" way: Manual filtering
const intersection = new Set(
  [...setA].filter(x => setB.has(x))
);

console.log(intersection); // Set { 'apples', 'bananas' }

It works, sure. But it’s clunky. You’re converting sets to arrays, running a filter, and then wrapping it back into a set. It’s performative overhead for something that should be a first-class citizen of the language.

The New Guard: Seven New Methods

The Set.prototype has been upgraded with seven new methods that handle the most common logical operations. Here is how the landscape looks now.

1. Combining Sets: union and intersection

These are the bread and butter of set logic. union merges everything; intersection keeps only what is common.

const internalUsers = new Set(['alice', 'bob']);
const externalUsers = new Set(['bob', 'charlie']);

// Give me everyone
const allUsers = internalUsers.union(externalUsers); 
// Set {'alice', 'bob', 'charlie'}

// Give me only the people in both categories
const hybrids = internalUsers.intersection(externalUsers);
// Set {'bob'}

2. Finding the Gap: difference and symmetricDifference

difference is the "A but not B" operator. symmetricDifference is the "In one, but not both" operator (effectively a logical XOR).

const mySkills = new Set(['JavaScript', 'CSS', 'Svelte']);
const jobRequirements = new Set(['JavaScript', 'CSS', 'React']);

// What do I have that they don't care about?
const extraSkills = mySkills.difference(jobRequirements);
// Set {'Svelte'}

// What makes us different? (Skills I lack + skills they don't require)
const gaps = mySkills.symmetricDifference(jobRequirements);
// Set {'Svelte', 'React'}

3. Comparison: isSubsetOf, isSupersetOf, and isDisjointFrom

These return booleans. They are incredibly useful for validation logic—like checking if a user has all the required permissions for an action.

const requiredPerms = new Set(['read', 'write']);
const userPerms = new Set(['read', 'write', 'delete']);

console.log(requiredPerms.isSubsetOf(userPerms)); // true
console.log(userPerms.isSupersetOf(requiredPerms)); // true

const restrictedPerms = new Set(['admin', 'root']);
console.log(userPerms.isDisjointFrom(restrictedPerms)); // true (No overlap)

A Real-World Use Case: Tagging Systems

Imagine you're building a blog UI. Users can filter posts by tags. You have a list of activeFilters and a list of postTags.

function shouldShowPost(postTags, activeFilters) {
  // If no filters are active, show everything
  if (activeFilters.size === 0) return true;

  // Does the post have at least one of the active filters?
  return !postTags.isDisjointFrom(activeFilters);
}

const myPostTags = new Set(['javascript', 'webdev']);
const userFilters = new Set(['react', 'javascript']);

console.log(shouldShowPost(myPostTags, userFilters)); // true

Before these methods, isDisjointFrom would have required a some() loop over an array. Now, the code reads exactly like the requirement: "Show the post if the tags are not disjoint from the filters."

The "Gotcha" You Need to Know

There is one specific detail that might trip you up: The methods accept "Set-like" objects, but not necessarily Arrays.

If you try to do mySet.union(['a', 'b']), it will throw a TypeError. The engine expects something that has a size property and looks like an iterator (like another Set, a Map, or a custom object).

However, this is actually a performance win. Because it expects a Set-like object, it can use the internal has() optimization of the second set instead of doing an O(n) scan of an array.

Why this actually matters (The Performance "Why")

You might think, "I've been using Lodash for this for years, why should I care?"

1. Bundle Size: Native methods are free. Every helper function you delete is a few bytes less for your users to download.
2. Engine Optimization: Browser engines (V8, SpiderMonkey) can optimize these operations at a much lower level than a JavaScript-based filter loop.
3. Readability: Code is read more often than it is written. a.intersection(b) communicates intent instantly. [...a].filter(i => b.has(i)) requires the reader to mentally parse the logic to realize it's an intersection.

Can I use this today?

Yes, mostly. As of mid-2024, these methods are supported in Chrome 122+, Safari 17+, and Firefox 127+. If you're targeting older browsers or Node.js environments (before version 22), you’ll still need a polyfill (like core-js).

But for modern web apps, the "Quiet Evolution" is complete. The JavaScript Set is finally a first-class citizen of logic. Use it.