JavaScript Map and Set: Key-Value Collections and the New Set Methods
We learn how Map differs from objects, unique values with Set, iteration, set operations such as union and intersection from ES2025, and ES2026's getOrInsert, with a quick look at WeakMap.
Version note: The code in this post targets ES2026 and current browsers (Chrome, Edge, Firefox, Safari).
Predict First
An object that gives prices to two fruits, and a Set full of repeated values:
const prices = {};
const apple = { id: 1 };
const pear = { id: 2 };
prices[apple] = 10;
prices[pear] = 20;
console.log(prices[apple]);
console.log(new Set([1, "1", 1, NaN, NaN]).size);
What does this print to the console? Make a guess for both lines before you read on. We'll give the first answer in the very next section and the second while covering Set.
The Limits of Using an Object as a Dictionary
We've used objects both as records (a user's name and email) and as dictionaries (stock by product code). They're great for the first job. When you use them as dictionaries, though, some corners get sharp.
The answer to the first line is 20. Object keys are converted to strings (or symbols). When you write prices[apple], the engine turns the apple object into a string, and the result is "[object Object]". pear turns into the very same string. So both lines wrote to the same key, and the second overwrote the first. Run Object.keys(prices) and you'll see a single key: ["[object Object]"].
There are smaller annoyances too. To find out how many keys an object has, you first need to build an array with Object.keys. An object can get mixed up with properties coming from its prototype. And it isn't optimized for large dictionaries with frequent additions and deletions. Map and Set, which arrived with ES2015, fill exactly this gap.
Map: A Dictionary Where Anything Can Be a Key
Think of a coat check. You hand over your coat and get a ticket; whoever shows the ticket gets the coat back. In a Map, that ticket can be anything: a number, a string, an object, even a DOM element. The key is whatever it is; it isn't converted to a string.
const ada = { name: "Ada" };
const alan = { name: "Alan" };
const lastSeen = new Map();
lastSeen.set(ada, "09:30").set(alan, "11:15");
console.log(lastSeen.get(ada)); // "09:30"
console.log(lastSeen.has(alan)); // true
console.log(lastSeen.size); // 2
lastSeen.delete(alan);
console.log(lastSeen.size); // 1
const mixed = new Map();
mixed.set(1, "number").set("1", "string");
console.log(mixed.get(1), mixed.get("1")); // "number" "string"
There are five core methods: set, get, has, delete, and clear, which removes everything. The number of entries is always ready in the size property. Since set returns the Map itself, you can chain calls. And the number 1 and the string "1" are now two separate keys.
You can also create a Map with initial data. The constructor expects an array of [key, value] pairs. That's exactly the shape of the output of Object.entries, which we saw earlier, so going from an object to a Map and back takes one line:
const settings = { theme: "dark", lang: "en" };
const settingsMap = new Map(Object.entries(settings));
console.log(settingsMap.get("theme")); // "dark"
const backToObject = Object.fromEntries(settingsMap);
console.log(backToObject); // { theme: "dark", lang: "en" }
Looping Over a Map
A Map keeps its entries in insertion order and can be looped over directly with for...of. Each step gives you a [key, value] pair; unpacking them into two variables with destructuring is the most readable way:
const stock = new Map([
["pen", 120],
["notebook", 0],
["thermos", 4]
]);
for (const [item, qty] of stock) {
console.log(`${item}: ${qty}`);
}
console.log([...stock.keys()]); // ["pen", "notebook", "thermos"]
console.log([...stock.values()]); // [120, 0, 4]
stock.forEach((qty, item) => console.log(item, qty));
keys(), values() and entries() return iterators, not arrays. If you want to use an array method, convert them with spread or Array.from. We'll cover iterators in detail in the Advanced series. Watch the parameter order in forEach: the value comes first, then the key.
getOrInsert: Give It to Me, or Add It
The pattern you write most often with a Map goes like this: "Does this key exist? If not, put in a starting value. Then get that value and use it." getOrInsert, which arrived with ES2026, brings those three steps down to a single call. Let's group words by their first letter:
const words = ["apple", "banana", "apricot", "blueberry"];
// The classic way
const groupsOld = new Map();
for (const word of words) {
if (!groupsOld.has(word[0])) groupsOld.set(word[0], []);
groupsOld.get(word[0]).push(word);
}
// ES2026
const groups = new Map();
for (const word of words) {
groups.getOrInsert(word[0], []).push(word);
}
console.log(groups); // Map(2) { "a" => ["apple", "apricot"], "b" => ["banana", "blueberry"] }
getOrInsert(key, value) returns the existing value if the key is there; otherwise it adds the value you gave and returns that. If producing the starting value is expensive, use its sibling getOrInsertComputed. It takes a function instead of a value and calls that function, with the key itself, only when the key is missing. It's a perfect fit for a simple cache:
const slugCache = new Map();
function toSlug(title) {
return slugCache.getOrInsertComputed(title, (t) => {
console.log("computing:", t);
return t.toLowerCase().replaceAll(" ", "-");
});
}
console.log(toSlug("Map and Set")); // "computing: Map and Set", then "map-and-set"
console.log(toSlug("Map and Set")); // "map-and-set", from the cache
The same two methods exist on WeakMap as well. They became Baseline "newly available" in early 2026 and work starting with Chrome and Edge 145, Firefox 144 and Safari 26.2. If you also need to support slightly older browsers, the classic has/set/get pattern above works everywhere.
Set: A Collection of Unique Values
A Set is a collection in which every value can appear at most once. Like a guest list at the door: try to write the same person down a second time and the list doesn't change. There are no separate keys and values; there are only values.
const tags = new Set();
tags.add("js").add("css").add("js");
console.log(tags.size); // 2
console.log(tags.has("css")); // true
tags.delete("css");
const unique = [...new Set([3, 1, 3, 2, 1])];
console.log(unique); // [3, 1, 2]
The shortest way to drop duplicates from an array is what the last line does: pass the array through a Set and turn it back into an array with spread. The order is preserved; each value stays where it was first seen.
The answer to the second opening question is here too: 3. A Set decides whether two values are the same using a rule called SameValueZero. It's almost the same as ===; the only difference is that it treats NaN as equal to itself. That's why 1 and "1" count as two separate values, while the two NaNs count as one: 1, "1" and NaN. The same rule applies to Map keys. With objects, though, references are compared, not contents; new Set([{ id: 1 }, { id: 1 }]).size is 2, because these are two separate objects that just look alike.
The New Set Methods: Set Operations
Remember the Venn diagrams from school: two circles, intersection, union, difference. For a long time Set couldn't do these operations; we wrote them all by hand with filter and has. ES2025 brought seven new methods:
const frontend = new Set(["html", "css", "js", "git"]);
const backend = new Set(["sql", "js", "git", "docker"]);
console.log(frontend.union(backend)); // Set(6) {"html", "css", "js", "git", "sql", "docker"}
console.log(frontend.intersection(backend)); // Set(2) {"js", "git"}
console.log(frontend.difference(backend)); // Set(2) {"html", "css"}
console.log(frontend.symmetricDifference(backend)); // Set(4) {"html", "css", "sql", "docker"}
console.log(new Set(["js", "git"]).isSubsetOf(frontend)); // true
console.log(frontend.isSupersetOf(new Set(["css"]))); // true
console.log(frontend.isDisjointFrom(new Set(["php"]))); // true
The first four return a new Set and leave the originals alone. union gives you everything in either one, intersection only the shared values, difference what's in the first but not the second, and symmetricDifference what's in only one of them. The last three return a boolean: is it a subset, a superset, or do they share nothing at all?
These methods became Baseline "newly available" in 2024 and have worked since Chrome and Edge 122, Firefox 127 and Safari 17, so you can use them with confidence in every current browser. They expect a Set as the argument (or a set-like object such as a Map); pass an array and you'll get a TypeError. If you have an array, wrap it with new Set(array) first.
Map or Object, Set or Array?
Map and Set don't send objects and arrays into retirement; each has its own job. For a record whose shape is known up front, such as a user with a name, an email and a role, use an object. For data that travels to and from a server as JSON, an object is the natural choice too. For a dictionary whose keys are decided at runtime, that gets frequent additions and deletions, has non-string keys, or whose size you check often, a Map fits better.
There's a similar split between arrays and Sets. If order, indexes and duplicate values matter, use an array; if uniqueness and the question "is this value in there?" matter, use a Set. On large collections, set.has() is usually much faster than array.includes(), because it doesn't walk through every element one by one.
There are also two cousins whose names you'll hear: WeakMap and WeakSet. They don't accept values like strings or numbers as keys, only objects; you can't loop over them, and they have no size. In return, they don't keep their key objects alive in memory; if nothing else can reach an object anymore, the garbage collector can clean it up. We'll cover this in detail in the memory management post of the Advanced series.
Common Pitfalls
Writing to a Map with square brackets. myMap["key"] = 1 looks like it works, but it doesn't add an entry to the Map; it writes an ordinary property on the Map object. myMap.size stays 0 and myMap.get("key") returns undefined. With a Map, always use set and get.
Turning a Map straight into JSON. JSON.stringify(new Map([["a", 1]])) gives you "{}"; the data silently disappears. Convert it to an object with Object.fromEntries(map) first. Sets have the same problem; turn them into an array with [...set] before sending.
Recreating an object key. lastSeen.get({ name: "Ada" }) returns undefined, because the object you just wrote isn't the same reference as the object you used as the key. In Maps with object keys, keep the key in a variable.
Passing arrays to Set methods. frontend.union(["php"]) throws a TypeError. An array isn't set-like; it lacks required members such as size and has.
A Small Experiment
★☆☆ Predict before running: const s = new Set("mississippi"); console.log(s.size, [...s].join("")); Remember that a string can be looped over too.
★★☆ Count how many times each word appears in a sentence using a Map. Write it first with get and ??, then with getOrInsert. Find the most frequent word and log it.
★★☆ The IDs of students who came to class on Monday are [3, 7, 12, 15], and on Tuesday [7, 9, 12, 20]. Use the Set methods to find who came on both days, who came only on Monday, and who came on at least one day.
★★★ Write a function called memoize(fn): it should store the results of a single-argument function in a Map and not repeat the calculation when called again with the same argument. Use getOrInsertComputed, and show with a console.log inside the function how many times it really runs.
We ran into TypeError several times in this post and always just said "it throws" and moved on. In the next post we'll focus on the errors themselves: catching them with try/catch/finally, throwing them with throw, defining your own error types and chaining errors together with cause.