JavaScript Set Methods: union, intersection, and difference
JavaScript Set methods are for membership questions, not fancy array replacement. If your program cares whether a value belongs to a group, Set is often the right data shape. If your program cares about duplicates, counts, or every original position, an array is probably still the right data shape.
Modern JavaScript gives Set its own methods for common membership operations: union, intersection, difference, symmetricDifference, isSubsetOf, isSupersetOf, and isDisjointFrom. These methods make code read closer to the question your program is asking.
MDN documents methods such as Set.prototype.intersection() as Baseline 2024 features. If your app supports older runtimes, check compatibility before relying on them directly.
The short rule
Use a Set when uniqueness and membership are the main ideas.
| Question | Method |
|---|---|
| What is in either group? | a.union(b) |
| What is in both groups? | a.intersection(b) |
What is in a but not b? | a.difference(b) |
| What is in exactly one group? | a.symmetricDifference(b) |
Is everything in a also in b? | a.isSubsetOf(b) |
Does a contain everything in b? | a.isSupersetOf(b) |
| Do the groups share nothing? | a.isDisjointFrom(b) |
The first four return a new Set. The last three return booleans.
const selected = new Set(["read", "write"]);
const required = new Set(["write", "deploy"]);
console.log(selected.union(required));
// Set(3) { "read", "write", "deploy" }
console.log(selected.intersection(required));
// Set(1) { "write" }
console.log(required.difference(selected));
// Set(1) { "deploy" }
The original sets are unchanged.
The old array way
Before these methods, developers often converted between arrays and sets by hand:
const selected = ["read", "write"];
const required = ["write", "deploy"];
const shared = selected.filter((permission) =>
required.includes(permission),
);
console.log(shared);
// ["write"]
That works for small arrays. It also hides the real question. The code says "filter one array by checking another array." The domain question is simpler:
Which permissions are in both groups?
With sets, the code says that:
const selected = new Set(["read", "write"]);
const required = new Set(["write", "deploy"]);
const shared = selected.intersection(required);
console.log([...shared]);
// ["write"]
That does not mean arrays are bad. It means the data shape should match the job.
For array method decisions, read map vs filter vs find and JavaScript Array Methods.
union: values from either set
union() returns a new set containing values from both sets.
const defaultModules = new Set(["variables", "strings"]);
const selectedModules = new Set(["strings", "arrays", "dom"]);
const visibleModules = defaultModules.union(selectedModules);
console.log([...visibleModules]);
// ["variables", "strings", "arrays", "dom"]
"strings" appears once because sets keep unique values.
The first set controls the starting order. New values from the second set are added after that.
const first = new Set(["a", "b"]);
const second = new Set(["b", "c"]);
console.log([...first.union(second)]);
// ["a", "b", "c"]
console.log([...second.union(first)]);
// ["b", "c", "a"]
Same membership, different iteration order. If the display order matters, do not pretend sets are order-free magic. They preserve insertion order.
intersection: values shared by both
intersection() returns values that appear in both sets.
const userPermissions = new Set(["read", "write", "billing"]);
const routePermissions = new Set(["billing", "admin"]);
const matches = userPermissions.intersection(routePermissions);
console.log([...matches]);
// ["billing"]
This is useful for permissions, tags, feature flags, search filters, and any check where "shared membership" is the point.
The array version is still valid:
const matches = userPermissionsArray.filter((permission) =>
routePermissionsArray.includes(permission),
);
But the set version makes membership explicit:
const matches = userPermissions.intersection(routePermissions);
If you need an array again for rendering, convert at the boundary:
const visiblePermissions = [...matches];
difference: values missing from the other set
difference() returns values from the first set that are not in the second.
const required = new Set(["email", "password", "profile"]);
const submitted = new Set(["email", "profile"]);
const missing = required.difference(submitted);
console.log([...missing]);
// ["password"]
The direction matters:
console.log([...required.difference(submitted)]);
// ["password"]
console.log([...submitted.difference(required)]);
// []
Read a.difference(b) as:
items in a after removing anything that appears in b
That phrasing prevents a lot of reversed-code bugs.
symmetricDifference: values that are not shared
symmetricDifference() returns values that appear in exactly one set.
const before = new Set(["draft", "review", "publish"]);
const after = new Set(["draft", "publish", "archive"]);
const changed = before.symmetricDifference(after);
console.log([...changed]);
// ["review", "archive"]
This is useful when you need "what changed" but do not care yet whether each change was added or removed.
If you do care about direction, use two differences:
const removed = before.difference(after);
const added = after.difference(before);
console.log([...removed]); // ["review"]
console.log([...added]); // ["archive"]
Boolean membership checks
The boolean methods answer relationship questions.
const required = new Set(["read", "write"]);
const admin = new Set(["read", "write", "delete"]);
const guest = new Set(["read"]);
Use isSubsetOf() when the left set must fit inside the right set:
console.log(required.isSubsetOf(admin)); // true
console.log(required.isSubsetOf(guest)); // false
Use isSupersetOf() when the left set must contain everything from the right set:
console.log(admin.isSupersetOf(required)); // true
console.log(guest.isSupersetOf(required)); // false
Use isDisjointFrom() when two sets must not overlap:
const publicRoutes = new Set(["home", "pricing"]);
const adminRoutes = new Set(["billing", "users"]);
console.log(publicRoutes.isDisjointFrom(adminRoutes)); // true
The method names are long, but they are honest. That is better than a clever filter().length === 0 chain that makes the next person decode your intention.
Sets compare values, not object contents
This is the trap that catches people coming from arrays of records.
const selected = new Set([{ id: 1, name: "Ada" }]);
const required = new Set([{ id: 1, name: "Ada" }]);
console.log(selected.intersection(required).size);
// 0
Those objects have the same contents, but they are different object references. Set compares object identity, not deep equality.
If membership is based on an id, store ids in the set:
const selectedIds = new Set([1]);
const requiredIds = new Set([1]);
console.log(selectedIds.intersection(requiredIds).size);
// 1
Or keep a map from id to object when you need records later:
const usersById = new Map(users.map((user) => [user.id, user]));
const selectedIds = new Set([1, 3]);
const selectedUsers = [...selectedIds].map((id) => usersById.get(id));
Set methods are great at membership. They are not a deep comparison engine.
Arrays still matter
Do not turn every array into a set because the method names look clean.
Use arrays when:
- duplicates are meaningful
- counts matter
- original positions matter
- sorted order matters more than membership
- you need
map,filter,reduce, orfindover records
Use sets when:
- each value should appear once
- membership checks are central
- you are comparing groups
- uniqueness is part of the contract
- you want relationship checks like subset, superset, or disjoint
Here is a bad set conversion:
const cartItems = ["apple", "apple", "banana"];
const cartSet = new Set(cartItems);
console.log([...cartSet]);
// ["apple", "banana"]
That destroyed the quantity. If the cart has two apples, a set is the wrong shape.
Here is a good set conversion:
const assignedTags = ["javascript", "arrays", "javascript"];
const uniqueTags = new Set(assignedTags);
console.log([...uniqueTags]);
// ["javascript", "arrays"]
In this case, duplicate tags are noise. The set expresses the rule.
For more on copying arrays and uniqueness, read The Spread Operator.
Convert at the boundary
Many APIs and UI components still want arrays.
That is fine. Keep the set where membership work happens, then convert at the edge.
function visibleTags(requiredTags, selectedTags) {
const required = new Set(requiredTags);
const selected = new Set(selectedTags);
const visible = required.intersection(selected);
return [...visible];
}
console.log(visibleTags(["js", "dom", "async"], ["dom", "css"]));
// ["dom"]
Inside the function, Set expresses the membership operation. At the return boundary, the function gives the caller an array because arrays are convenient for rendering, JSON, and most UI code.
That is often the cleanest pattern:
array input -> set operation -> array output
Practice the decision
Before choosing an array pipeline or a set method, ask what the result means.
Do I need every transformed item?
Use map.
Do I need every item that passes a rule?
Use filter.
Do I need the first item that passes a rule?
Use find.
Do I need group membership?
Use Set.
The point is not to collect methods. The point is to choose the data shape that makes the code tell the truth.
Key takeaways
union,intersection,difference, andsymmetricDifferencereturn new sets without mutating the originals.isSubsetOf,isSupersetOf, andisDisjointFromreturn booleans for relationship checks.- Sets are best when uniqueness and membership are the core idea.
- Arrays are still better when duplicates, counts, positions, or record transformations matter.
- Sets compare objects by reference, so store ids or primitive keys when membership is based on object contents.