Set operations
Use the ES2025 Set methods for union, intersection, difference, relationship checks, and set-like inputs without mutating your original data.
- 01Compare two Sets with one callUse
union,intersection,difference, andsymmetricDifferenceinstead of building the answer by hand from arrays. - 02Ask yes-or-no questionsUse
isSubsetOf,isSupersetOf, andisDisjointFromto check permissions, tags, and shared values. - 03Know what you can pass inExplain why a Map or a home-made set-like object works, and why an array must be wrapped in
new Set()first.
Sets as friend groups
A Set is a collection where each value appears only once. If you add the same value twice, the Set still holds it once.
Often you have two Sets and want to compare them. JavaScript's 2025 edition, called ES2025, added seven methods for this. Four of them make a new Set:
union: every value from either Set.intersection: only the values both Sets have.difference: the values only the first Set has.symmetricDifference: the values only one of the two Sets has.
The other three answer a yes-or-no question with true or false. You'll meet them in their own section.
Before these methods, you had to build each answer by hand from arrays, filter, and includes. Now the method name says what you want.
Two friend groups arrive at a party. You can ask four questions. Who is in either group? Who is in both groups? Who is only with my group? Who is in just one of the groups? Each question is one of the four methods.
- In real life: Everyone either group invited
- In JavaScript:
a.union(b) - In real life: Friends both groups know
- In JavaScript:
a.intersection(b) - In real life: People only my group knows
- In JavaScript:
a.difference(b) - In real life: Everyone except the shared friends
- In JavaScript:
a.symmetricDifference(b)
These methods are new. Node 22 and up-to-date browsers have them. An older browser may not. The playground below checks for them once the page has loaded. If they are missing, use the older way shown later in this lesson, with arrays and filter/includes. Or load a polyfill: a trusted script that adds a missing feature.
const cart = new Set(["tea", "bread"]);
const newItems = new Set(["bread", "eggs"]);
const allItems = cart.union(newItems);
console.log([...allItems].join(", "));Line 1 makes a Set called cart with two groceries. Line 2 makes a Set of new items. One of them, bread, is already in the cart. Line 3 calls union. It makes a new Set with the values of both. Line 4 prints that new Set. The spread [...allItems] copies the values into an array so join can put commas between them.
It prints tea, bread, eggs. The cart's values come first, in their order. Then come the new values. bread appears only once.
union puts the values of both Sets into a new Set. bread is in both, but the new Set holds it only once. cart and newItems stay as they were.const hasSetMethods = typeof Set.prototype.union === "function";
console.log(hasSetMethods);Line 1 asks whether Set.prototype.union is a function. Set.prototype is the place where all Sets find their methods, and typeof tells you what kind of value something is. A browser that has the method prints true.
This kind of check is called feature detection. It is safer than guessing what a browser supports. If the check gives false, use the older way shown later.
This lesson uses ideas from earlier lessons:
- Map & Set, for how a Set works.
- Searching arrays, for the older
filter/includesway. - map/filter/reduce, for turning one list into another.
Union & intersection
INTERACTIVEIn a.union(b), two Sets take part. This lesson gives them names:
- The caller is the Set before the dot,
a. It calls the method. - The argument is the Set inside the brackets,
b.
a.union(b) makes a new Set. First it takes every value from the caller, in order. Then it adds the values from the argument that are not in it yet. So when you turn the result into an array, the caller's values come first.
a.intersection(b) makes a new Set with only the values that are in both Sets. A simple way to picture it: walk through the caller and keep each value the argument also has.
When the argument is the smaller Set, JavaScript may walk through the argument instead, to save time. Then the result can come out in a different order. So use intersection to find shared values, not to put values in order.
const devA = new Set(["JavaScript","CSS","Accessibility"]);const devB = new Set(["CSS","React","Testing"]);const result = [...devA.union(devB)];console.log(result);Developer A
Developer B
[JavaScript, CSS, Accessibility, React, Testing][JavaScript, CSS, Accessibility][CSS, React, Testing]Checking whether this browser has the new Set methods. The check waits until the page has loaded.
The circles are a drawing to help you see the two groups. The result list next to them is real. It comes from the actual method in the code. The rows Original A and Original B show that the method never changes the two Sets it compares.
Step through the four methods that make a new Set. Each result comes from the real method running in your browser.
script
const yourFriends = new Set(["Bea", "Cal", "Dev"]);myFriends.union(yourFriends);myFriends.intersection(yourFriends);myFriends.difference(yourFriends);myFriends.symmetricDifference(yourFriends);Difference & symmetricDifference
STEP THROUGHa.difference(b) keeps the values that are in the caller, a, but not in the argument, b. So which Set comes first matters.
oldTags.difference(newTags)answers "which tags were removed?"newTags.difference(oldTags)answers "which tags were added?"
The two Sets are the same in both lines. Only the caller changes, and so does the question.
js and accessibility are in both Sets, so difference drops them. The Set before the dot decides which side you keep: css from the old tags, or react from the new tags.symmetricDifference keeps the values that are in exactly one of the two Sets. It drops the values both Sets share. Use it when you only want one list of "what changed". When you need the added values and the removed values as two separate lists, call difference twice, once each way.
Use the setting to choose which Set comes before the dot. Watch line 3: difference gives a different answer each way.
script
const newTags = new Set(["js", "react", "accessibility"]);oldTags.difference(newTags);oldTags.isSupersetOf(newTags);oldTags.isDisjointFrom(newTags);You write down what is in your backpack before a trip, and again after it. Compare the lists one way, and you find what you lost. Compare them the other way, and you find what you picked up. If you only want to know what changed, you make one list of both.
- In real life: Packed before but not after
- In JavaScript:
before.difference(after)finds removed items - In real life: Packed after but not before
- In JavaScript:
after.difference(before)finds added items - In real life: Changed either way
- In JavaScript:
before.symmetricDifference(after)
isSubsetOf, isSupersetOf & isDisjointFrom
These three methods don't make a new Set. They give back a boolean: true or false. Use them when your question has a yes-or-no answer.
small.isSubsetOf(big): is every value insmallalso inbig?big.isSupersetOf(small): doesbighave every value thatsmallhas?a.isDisjointFrom(b): do the two Sets share no values at all?
const required = new Set(["read", "write"]);
const user = new Set(["read", "write", "delete"]);
console.log(required.isSubsetOf(user));
console.log(user.isSupersetOf(required));
console.log(required.isDisjointFrom(user));Line 1 lists the permissions a page needs. Line 2 lists the permissions a user has. Lines 3 and 4 ask the same question from opposite sides: does the user have everything required? Both print true.
Line 5 asks whether the two Sets share nothing. They share read and write, so it prints false.
required is also in user. So required fits inside user, and both questions answer true. The extra delete in user doesn't matter.These methods only say yes or no. When a check gives false and you want to know which values caused it, use difference.
Every member of the book club is a student at the school. You can say this two ways. The club fits inside the school. The school holds the whole club. Two sports teams are separate only if no student plays on both.
- In real life: Every book club member attends the school
- In JavaScript:
bookClub.isSubsetOf(school) - In real life: The school contains the whole book club
- In JavaScript:
school.isSupersetOf(bookClub) - In real life: Two teams share nobody
- In JavaScript:
teamA.isDisjointFrom(teamB)
const required = new Set(["read", "write"]);
const user = new Set(["read", "write", "delete"]);
if (required.isSubsetOf(user)) {
console.log("allowed");
}Line 4 reads like the question you are asking: are all the required permissions inside the user's permissions? Here they are, so line 5 prints allowed. Writing user.isSupersetOf(required) gives the same answer. Pick the one that matches how you say the question.
const user = new Set(["read", "write"]);
const required = new Map([["read", true]]);
console.log(user.isSupersetOf(required));A Map holds pairs of a key and a value. When a Map is the argument, the method looks only at its keys. The value true is ignored. The only key is read, and user has read, so line 3 prints true.
Set-like objects
STEP THROUGHThe argument doesn't have to be a real Set. It has to be set-like. A set-like object has these three things:
- a
sizeproperty that is a number, - a
has(value)method that says whether a value is in it, - a
keys()method that hands out its values one by one.
A Map has all three. Its keys play the part of the values.
These methods take set-like objects, not everything you can loop over. Step through a Map, a home-made object, and an array mistake.
script
const requiredMap = new Map([["read", true], ["write", true]]);userPermissions.isSupersetOf(requiredMap);const requiredLike = { size: 2, has(value) { return value === "read" || value === "write"; }, keys() { return ["read", "write"][Symbol.iterator](); },};userPermissions.isSupersetOf(requiredLike);userPermissions.union(["read"]);const userPermissions = new Set(["read", "write", "admin"]);const requiredMap = new Map([["read", true], ["write", true]]);userPermissions.isSupersetOf(requiredMap);const requiredLike = { size: 2, has(value) { return value === "read" || value === "write"; }, keys() { return ["read", "write"][Symbol.iterator](); },};userPermissions.isSupersetOf(requiredLike);userPermissions.union(["read"]);Lines 4 to 8 make a plain object with size, has, and keys. It is not a Set, but it has the three things, so line 9 works too.
You can loop over an array with for...of. Things you can loop over like that are called iterable. But an array is not set-like, because it has no has(value) method. So line 10 throws a TypeError. When your values start in an array, wrap it first: mySet.union(new Set(array)).
size, has(), and keys(). A Map has all three, and its keys are the values. An array has no has(), so the call throws a TypeError.The old way vs the new way
Before these methods, you built each answer by hand. You copied a Set into an array with spread, filtered the other one, and checked each value with has or includes. That still works. Use it when a browser doesn't have the new methods. The new methods are easier to read and easier to get right.
const a = new Set(["js", "css", "html"]);const b = new Set(["css", "react"]);const newWay = [...a.union(b)];const oldWay = [...a, ...[...b].filter((item) => !a.has(item))];console.log(newWay.join(", "));console.log(oldWay.join(", "));console.log([...a].join(", "));Line 3 is the new way: one call to union. Line 4 is the old way. It copies every value of a, then adds each value of b that a doesn't have. Lines 5 and 6 print the same list.
Line 7 prints a again, and it is still js, css, html. These methods make new Sets. They never change the Sets you start with.
The table below lists all seven methods. Every row uses the two party groups: a holds Ada, Bea, and Cal, and b holds Bea, Cal, and Dev.
| When you write | What you get back | What it keeps or asks | Result for the party groups |
|---|---|---|---|
a.union(b) | A new Set. | It keeps every value from either Set. | ['Ada', 'Bea', 'Cal', 'Dev'] |
a.intersection(b) | A new Set. | It keeps only the values both Sets have. | ['Bea', 'Cal'] |
a.difference(b) | A new Set. | It keeps the values only a has. | ['Ada'] |
a.symmetricDifference(b) | A new Set. | It keeps the values only one of the two Sets has. | ['Ada', 'Dev'] |
a.isSubsetOf(b) | true or false. | It asks: is every value in a also in b? | false, because Ada is not in b. |
a.isSupersetOf(b) | true or false. | It asks: does a have every value in b? | false, because Dev is not in a. |
a.isDisjointFrom(b) | true or false. | It asks: do the two Sets share no values? | false, because they share Bea and Cal. |
- Everyone invited by either friend group
- People both groups already know
- Tags that were removed from the old list
- Tags that appear on exactly one side
- Does this user have every required permission?
- Do these two teams share nobody?
- People only my friend group knows
- Does the school roster contain the whole book club?
Sort each question by the kind of answer it needs. Put a card in “Order matters” when swapping the caller and the argument would change the question.
Where you'll use this
You'll use these methods whenever a list says who or what belongs to a group. For example:
- tags on a blog post,
- a user's permissions,
- the filters someone ticked on a shopping site,
- features switched on for a user,
- ingredients in a recipe,
- hobbies two people share.
const before = new Set(["js", "css", "accessibility"]);
const after = new Set(["js", "react", "accessibility"]);
const added = after.difference(before); // Set { "react" }
const removed = before.difference(after); // Set { "css" }
const changed = before.symmetricDifference(after); // Set { "css", "react" }const mia = new Set(["music", "hiking", "games"]);
const noor = new Set(["cooking", "music", "games"]);
const shared = mia.intersection(noor); // Set { "music", "games" }A good habit: say the question in words, and let the variable name say it too. "Added tags" is after.difference(before). "Removed tags" is before.difference(after). A clear name stops someone from swapping the two Sets by mistake later.
Common misconceptions
“Union changes the first Set.”
No. union, intersection, difference, and symmetricDifference each make a new Set. The Sets you start with stay the same.
“Difference works the same both ways.”
No. a.difference(b) keeps the values only a has. Swap the two Sets and you ask a different question.
“Arrays count as set-like because you can loop over them.”
No. Set-like means it has size, has, and keys. Arrays have no has. Wrap an array first: new Set(array).
“Intersection always keeps the caller’s order.”
It often does in small examples. But when the argument is smaller, JavaScript may walk through the argument instead. Don't use intersection to put values in order.
“Two objects that look the same are the same Set value.”
No. An object matches only itself. Two separate { id: 1 } objects are two different values to a Set.
const savedUser = { id: 1 };
const currentUser = { id: 1 };
const saved = new Set([savedUser]);
console.log(saved.has(savedUser));
console.log(saved.has(currentUser));Lines 1 and 2 make two separate objects that look the same. Line 3 puts only the first one in a Set. Line 4 asks about that same object, so it prints true. Line 5 asks about the other object. It looks the same, but it is a different object, so it prints false.
To keep a result, store it in a variable with a clear name: const added = after.difference(before). The two Sets you started with don't change, so you can still use them for other checks.
Practice exercises
5 EXERCISESWhat do the two lines print?
const a = new Set(["red", "blue", "green"]);
const b = new Set(["blue", "gold"]);
console.log([...a.union(b)].join(", "));
console.log([...a.intersection(b)].join(", "));The union is red, blue, green, gold. blue is in both Sets, but it appears only once. The intersection is just blue.
Does the user have every required permission? What does it print?
const required = new Set(["read", "write"]);
const user = new Set(["read", "write", "delete"]);
console.log(required.isSubsetOf(user));required.isSubsetOf(user) is true, because both required values, read and write, are in the user's Set.
Type the added tag, then the removed tag.
const before = new Set(["js", "css", "accessibility"]);
const after = new Set(["js", "react", "accessibility"]);
console.log([...after.difference(before)].join(", "));
console.log([...before.difference(after)].join(", "));after.difference(before) prints react. before.difference(after) prints css.
The starter code passes an array to union. Fix it by wrapping the array in a Set. Then predict what it prints.
const favorites = new Set(["tea", "coffee"]);
const incoming = ["coffee", "juice"];
console.log([...favorites.union(incoming)].join(", "));const favorites = new Set(["tea", "coffee"]);
const incoming = ["coffee", "juice"];
console.log([...favorites.union(new Set(incoming))].join(", "));An array has no has method, so union(incoming) throws a TypeError. union(new Set(incoming)) works and prints tea, coffee, juice.
Which recipe ingredients are already in the pantry?
const pantry = new Set(["flour", "eggs", "milk", "sugar"]);
const recipe = new Set(["eggs", "butter", "sugar"]);
console.log([...pantry.intersection(recipe)].join(", "));pantry.intersection(recipe) gives eggs and sugar, in the order they appear in the pantry Set.
Quiz: check your understanding
7 QUESTIONSQuestion 1 of 7What does
unionreturn?Choose an answer to see the explanation.
Question 2 of 7What does the difference snippet print?
Read the code, then predictconst a = new Set(["a", "b", "c"]); const b = new Set(["b", "d"]); console.log([...a.difference(b)].join(""));Choose an answer to see the explanation.
Question 3 of 7Which call answers: does the user have every required permission?
Choose an answer to see the explanation.
Question 4 of 7What does the subset snippet print?
Read the code, then predictconst school = new Set(["Mina", "Omar", "Pia"]); const bookClub = new Set(["Mina", "Pia"]); console.log(bookClub.isSubsetOf(school));Choose an answer to see the explanation.
Question 5 of 7Which argument is set-like for these Set methods?
Choose an answer to see the explanation.
Question 6 of 7What does the symmetric difference snippet print?
Read the code, then predictconst a = new Set(["JS", "CSS"]); const b = new Set(["CSS", "HTML"]); console.log([...a.symmetricDifference(b)].join(","));Choose an answer to see the explanation.
Question 7 of 7Do these methods change the original Sets?
Choose an answer to see the explanation.
Key takeaways
union,intersection,difference, andsymmetricDifferenceeach make a new Set.- With
difference, order matters. Swap the two Sets to get the other side. isSubsetOf,isSupersetOf, andisDisjointFromgive backtrueorfalse.- The argument must be set-like: it needs
size,has, andkeys. A Map works. Wrap an array innew Set(array)first. - Older browsers may not have these methods. Check for them before you use them.
Remember the one-liner.
Set methods compare two Sets for you: join them, find what they share, find what one has, or answer yes or no, and the original Sets never change.
Up next: WeakMap & WeakSet.