Map & Set
Learn when to choose Map, Set, or a plain object: key types, uniqueness, iteration order, and the new getOrInsert upsert helpers browsers are rolling out.
- 01Choose the right collectionPick a Map or a plain object, and a Set or an array, for a job.
- 02Loop over Maps and SetsUse
for...of,keys,values,entries, destructuring, andforEachwithout mixing up the order of key and value. - 03Get a value or add it in one stepCheck whether the browser has the new
getOrInsertmethods, and use a small helper when it doesn't.
Maps, Sets, and phone contacts
JavaScript already has objects and arrays. This lesson adds two more ways to keep a group of values: Map and Set. Each one is a collection, which means a container that holds many values.
- A Map keeps pairs. Each pair has a key, the thing you look up, and a value, the thing you get back. The key can be any value: a string, a number, even an object.
- A Set keeps single values. Each value can be in it only once.
This lesson uses ideas from earlier lessons: objects, object references, comparisons, loops over arrays, and destructuring. You will use Maps and Sets to count words, to remember what you have already seen, and to look things up.
Think of the contact list on your phone. You find a person by the name you saved, and you get one phone number back. A Map works the same way: you give it a key, and it gives you one value.
- In real life: You find a contact by their saved name
- In JavaScript: You find a Map value by its key
- In real life: Each name gives you one phone number
- In JavaScript: Each key gives you one value
- In real life: The "recently added" list shows contacts in the order you saved them
- In JavaScript: A Map keeps its pairs in the order you added them
A party has a guest list. If Ada is already on the list, writing Ada again does not add a second Ada. You can also ask, "Is Ada on the list?" The list cares about who is on it, not about who is first or third.
- In real life: Writing a name twice still means one guest
- In JavaScript: Adding the same value twice keeps one copy in the Set
- In real life: You can ask if a name is on the list
- In JavaScript:
set.has(value)answerstrueorfalse - In real life: Names stay in the order they were first written
- In JavaScript: A Set keeps values in the order you first added them
A Map stores key-value pairs, and a key can be any value. A Set stores values, each one only once. You can loop over both with for...of, and both keep the order in which you added things.
const prices = new Map([["tea", 3], ["coffee", 5]]);
console.log(prices.get("tea"));
console.log(prices.size);Line 1 makes a Map with two pairs. new Map(...) takes a list of [key, value] pairs. Here the key "tea" goes with 3, and the key "coffee" goes with 5. Each pair in a Map is also called an entry.
Line 2 calls get("tea"), which looks up the key "tea". It prints 3. Line 3 reads size, the number of entries in the Map. It prints 2.
get takes a key, follows it, and gives back that key's value.const tags = new Set(["news", "news", "sale"]);
console.log(tags.has("sale"));
console.log(tags.size);Line 1 makes a Set from an array. The array has "news" twice, but the Set keeps it only once. Line 2 calls has("sale"), which asks "is this value in the Set?" It prints true. Line 3 prints 2, because the Set holds two different values.
"news" twice. The Set already holds "news", so the second copy is skipped. Two values are left.So a Map gives you back a value for a key. A Set only answers yes or no: is this value in it?
Map vs plain objects
INTERACTIVEA plain object is still the best choice for many things. Use one for a record: one thing with fields you know ahead of time. A user profile, a product from a server, and settings you save as JSON are all records.
But an object's keys can only be strings or symbols. (A symbol is a special kind of key that is always unique. You won't need symbols here.) Any other key is turned into a string first:
obj[1]uses the key"1", which is a string.obj[]usually uses the key"[object Object]".
A Map does not change its keys. The number 1, the string "1", an object, and NaN are four different keys.
1 into the string "1", so both keys land on the same spot. A Map keeps each key as it is.A Map also has no hidden keys. An object borrows some names, like toString, from its prototype, the parent object it gets shared methods from. A Map holds only the entries you add, so map.has("toString") is false.
Guess which object keys land on the same spot. Then step through the same keys in a Map.
script
const obj = {};obj[1] = "number one";obj["1"] = "string one";obj[key] = "object key";obj[NaN] = "not a number"; const map = new Map();map.set(1, "number one");map.set("1", "string one");map.set(key, "object key");map.set(NaN, "not a number"); console.log(Object.keys(obj));console.log(map.size);const key = { id: 1 };const obj = {};obj[1] = "number one";obj["1"] = "string one";obj[key] = "object key";obj[NaN] = "not a number"; const map = new Map();map.set(1, "number one");map.set("1", "string one");map.set(key, "object key");map.set(NaN, "not a number"); console.log(Object.keys(obj));console.log(map.size);4[["1","number one"],["\"1\"","string one"],["{ id: 1 }","object key"],["NaN","not a number"]]falseThe Map keeps 1, "1", the object, and NaN as four different keys.
The table below compares the two. Read each row as: for this question, here is the object's answer, and here is the Map's answer.
| Your question | Plain object | Map |
|---|---|---|
| Which keys can I use? | Only strings and symbols. Any other key is turned into a string. | Any value: objects, functions, numbers, strings, NaN, and more. |
| In what order do I get the keys back? | In the object's own order. Number-like keys come first, from small to big. Other keys keep the order you added them. | In the order you added the entries. |
| How do I count the entries? | Use Object.keys(obj).length. It counts the object's own string keys that loops can see. | Read map.size. |
| How do I loop over it? | Use Object.keys, Object.values, or Object.entries. A for...in loop also works, but it can show keys from the prototype too. | Loop over the Map itself with for...of, or use .keys(), .values(), or .entries(). |
| Can I save it as JSON? | Yes. JSON.stringify(obj) works right away. | Not directly. Turn it into an object or an array first. |
| Will I add and remove keys often? | It works, but a list that keeps changing is harder to follow in an object. | Yes. A Map is made for keys that come and go. |
const stock = new Map([["tea", 4]]);
stock.set("coffee", 0);
console.log(stock.has("coffee"));
console.log(stock.get("coffee"));Line 1 makes a Map with one entry. Line 2 adds the key "coffee" with the value 0. Line 3 calls has("coffee") and prints true, because the key is there. Line 4 calls get("coffee") and prints 0.
Watch out: 0 counts as false in an if. A value like that is called falsy; 0, "", and undefined are falsy. So to ask "is this key here?", use has. Don't check whether get gave back something that counts as true.
Use an object for one record with field names you know ahead of time. Use a Map when you add and remove entries often, when your keys are not strings, or when you are building a lookup table: a list where you find a value by its key.
A Set keeps each value once
STEP THROUGHA Set answers one question very well: "Have I seen this value before?"
When you make a Set from an array, the repeats disappear. Each value stays where it first appeared. Removing repeats like this is often called deduping. Here is a common way to dedupe an array: [...new Set(array)]. The ... is spread syntax. It takes the values out of the Set and puts them into a new array.
Step through a Set and watch the repeats disappear. The values are not sorted.
script
const unique = new Set(list);unique.add(3).add(4);console.log(unique.size);console.log([...unique]);How does a Set decide that two values are the same? It uses a rule called SameValueZero, which you met in the Comparisons lesson. It is almost the same as ===:
NaNcounts as equal toNaN. With===, it does not.0and-0count as the same value, just like with===.- Objects are compared by reference, which means by which object it is, not by what is inside. Two separate
{}objects are two different values, even though they print the same.
const seen = new Set(["red", "red", "blue"]);console.log(seen.size); // 2console.log(seen.has("red")); // trueseen.add("green").add("red"); // add returns the Setconsole.log(seen.delete("red")); // trueconsole.log([...seen]); // ["blue", "green"]Line 1 makes a Set that holds "red" once and "blue" once. Then, line by line:
- Line 2:
sizecounts the values. It prints2. - Line 3:
haschecks whether a value is in the Set. It printstrue. - Line 4:
addputs a value in and gives back the same Set. So you can calladdagain right after it. Adding"red"again changes nothing. - Line 5:
deleteremoves a value. It printstruebecause it found one to remove. - Line 6: spreading the Set gives
["blue", "green"], in the order the values were added.
const first = { id: 1 };
const second = { id: 1 };
const selected = new Set([first, first, second]);
console.log(selected.size);
console.log(selected.has(first));Lines 1 and 2 make two separate objects. They have the same field, but each { } makes a new object. Line 3 puts first in twice and second once. The second first is the very same object, so the Set skips it.
Line 4 prints 2. Line 5 prints true, because first is in the Set.
Looping over a Map or Set
STEP THROUGHTo iterate means to go through the items one by one, for example with a for...of loop. Maps and Sets are iterable: for...of, spread (...), and destructuring all work on them.
A Set gives you its values. A Map gives you its entries, and each entry is a small array: [key, value]. So you can unpack each entry right in the loop: for (const [key, value] of map).
Follow the order: the Map's entries, then forEach with the value first, then a Set made from the keys.
script
["Ada", 3], ["Lin", 5],]); for (const [name, score] of scores) { console.log(name + ": " + score);} scores.forEach((value, key) => { console.log(key + " scored " + value);}); const names = new Set(scores.keys());console.log([...names].join(", "));You can also turn one kind of collection into another:
new Map(Object.entries(obj))turns an object into a Map.Object.entriesgives the object's[key, value]pairs.Object.fromEntries(map)turns a Map back into an object. This works well when the keys are strings.[...set]and[...map.keys()]make arrays. Then you can use array methods likemapandfilter.
const profile = { name: "Asha", role: "editor" };
const entries = new Map(Object.entries(profile));
console.log(entries.get("role"));
console.log(Object.fromEntries(entries));Line 1 is a plain object. Line 2 passes its pairs to new Map. Line 3 looks up "role" and prints editor. Line 4 turns the Map back into an object with Object.fromEntries, so it prints the object { name: "Asha", role: "editor" }.
A Map's forEach method calls your function once for each entry. It passes the value first and the key second: (value, key). In a for...of loop, each entry is [key, value], key first. The two look alike, so say the order out loud while you learn it.
Get it or add it: getOrInsert
INTERACTIVEA common job is: "Give me the value for this key. If there is none yet, add one first." This is called an upsert, a mix of the words "update" and "insert".
Two new Map methods do this job: Map.prototype.getOrInsert and Map.prototype.getOrInsertComputed. They were added to JavaScript in 2026, and not every browser has them yet. So after the page loads, it checks whether your browser has them. Checking first like this is called feature detection. If the methods are missing, the lesson uses a small helper function that does the same thing. Node 22 also needs the helper.
const counts = new Map();for (const word of words) { const box = getOrInsertComputed(counts, word, () => ({ count: 0 })); box.count += 1;}console.log([...counts]);checkingchecking[["theme","dark"],["letters",7]]["letters"]The page checks your browser only after it has loaded. The server that builds the page can't know which browser you use.
Here is what each one does:
getOrInsert(map, key, value): ifkeyis missing, it storesvalue. Then it returnsmap.get(key).getOrInsertComputed(map, key, callback): ifkeyis missing, it callscallback(key)and stores what the callback returns. A callback is a function you hand over so it can be called later. Then it returnsmap.get(key).
If the key is already there, both return the stored value. Nothing is replaced, and the callback is not called.
The real methods are called on the Map itself, like map.getOrInsert(key, value). The helpers below take the Map as their first input instead.
function getOrInsert(map, key, value) { if (!map.has(key)) map.set(key, value); return map.get(key);} function getOrInsertComputed(map, key, callback) { if (!map.has(key)) map.set(key, callback(key)); return map.get(key);} const cache = new Map();getOrInsert(cache, "theme", "dark");getOrInsert(cache, "theme", "light");getOrInsertComputed(cache, "letters", key => key.length);console.log([...cache]); // Helper proof: same=10, added=5Lines 1 to 9 are the two helpers. Lines 12 and 13 use the same key, "theme". Line 12 finds no "theme", so it stores "dark". Line 13 finds "dark" already there, so "light" is not used.
Line 14 finds no "letters" key, so it calls the callback. The callback returns 7, the length of the word "letters". Line 15 prints [["theme", "dark"], ["letters", 7]].
"theme" key, so "dark" goes in. Line 13 finds the key already there, so "light" is not used. Both calls return "dark".The last line of the code is a note from this page. It ran the helpers on a Map that held score with the value 10. same=10 means the old value was kept. added=5 is the length of the new key, "bonus".
Where you’ll use this
INTERACTIVEMaps and Sets are everyday tools for getting data ready to show on a page. Some examples:
- A search page keeps a Set of the filter IDs the user has picked.
- A stats panel counts events in a Map.
- A component library keeps measured sizes in a Map, with each page element (an object) as the key. Saving results so you don't have to work them out again is called a cache.
- A form turns a Map into an object before it saves it.
const counts = new Map();for (const word of words) { const current = counts.get(word) ?? 0; counts.set(word, current + 1);}console.log([...counts]);31The Map has 2 different words as keys. A word you type again adds one to its count. It does not add a new entry.
const counts = new Map();
for (const word of ["tea", "tea", "cake"]) {
counts.set(word, (counts.get(word) ?? 0) + 1);
}
console.log([...counts]);For each word, line 3 reads the current count. ?? 0 means "use 0 if there is no count yet". Then it saves the count plus one. Line 5 prints [["tea", 2], ["cake", 1]], in the order the words first appeared.
- Word counters: a Map from each word to its count. It keeps the order the words first appeared.
- Removing repeats: make a Set from an array, then spread it back into an array.
- Caches with object keys: a Map keeps every key and value in memory until you delete them. The later WeakMap & WeakSet lesson shows a kind of Map that lets them go.
- Saving as JSON: turn the Map into an array or an object first. The JSON lesson explains why a Map can't be saved directly.
Map, Set, or plain object?
SORTTo pick one, ask which question your data answers:
- "What named fields describe this one thing?" Use a plain object.
- "What value goes with this key?" Use a Map.
- "Have I seen this value before?" Use a Set.
Sort the cases below.
- A user record with fixed fields, saved as JSON
- Remember data for each page element or other object
- Keep a list of tags with no repeats
- Count words as
word → countpairs - App settings loaded and saved as JSON
- Remember which product IDs you have already seen
Put each case under the collection that fits it best.
Common misconceptions
- "A Map is just a fancier object." Map entries are not properties. Keys that are not strings stay as they are, and
Object.keys(map)does not list the entries. - "A Set compares objects by what is inside them." It checks whether it is the very same object. Two
{}objects are two different values. - "A Set sorts its values." A Set removes repeats, but it keeps the order you added things. It never sorts.
- "JSON.stringify(new Map()) saves the entries." It does not: it gives
"{}". Turn the Map into an array first, or useObject.fromEntrieswhen the keys are strings. - "getOrInsertComputed always runs the callback." It runs the callback only when the key is missing. That is the reason to use it: the work happens only once.
Practice exercises
5 EXERCISESWhat does this code print?
const ids = [7, 7, 2, 9, 2];
console.log([...new Set(ids)].join(","));const ids = [7, 7, 2, 9, 2];
console.log([...new Set(ids)].join(","));The Set keeps 7, then 2, then 9. The later copies are skipped, so it prints 7,2,9.
Run the counter in your head. What are the two printed lines?
const counts = new Map();
for (const name of ["Ada", "Lin", "Ada"]) {
counts.set(name, (counts.get(name) ?? 0) + 1);
}
console.log(counts.get("Ada"));
console.log(counts.size);const counts = new Map();
for (const name of ["Ada", "Lin", "Ada"]) {
counts.set(name, (counts.get(name) ?? 0) + 1);
}
console.log(counts.get("Ada"));
console.log(counts.size);Ada appears twice, so counts.get("Ada") is 2. The Map has two keys, Ada and Lin, so size is 2 too.
What exact JSON text is printed?
const map = new Map([["theme", "dark"], ["page", "home"]]);
console.log(JSON.stringify(Object.fromEntries(map)));const map = new Map([["theme", "dark"], ["page", "home"]]);
console.log(JSON.stringify(Object.fromEntries(map)));The Map's entries become the object's properties. Then JSON.stringify prints the object as {"theme":"dark","page":"home"}.
Predict map.size after these pairs are added.
const key = {};
const map = new Map([[1, "number"], ["1", "string"], [key, "object"], [NaN, "nan"], [NaN, "again"]]);
console.log(map.size);const key = {};
const map = new Map([[1, "number"], ["1", "string"], [key, "object"], [NaN, "nan"], [NaN, "again"]]);
console.log(map.size);The keys are the number 1, the string "1", the object, and NaN. The second NaN only replaces the value of the first NaN entry, so size is 4.
Use what the helper does to predict the two printed lines.
const calls = [];
function getOrInsertComputed(map, key, callback) {
if (!map.has(key)) map.set(key, callback(key));
return map.get(key);
}
const cache = new Map();
getOrInsertComputed(cache, "Ada", name => { calls.push(name); return name.length; });
getOrInsertComputed(cache, "Ada", name => { calls.push(name); return 99; });
console.log(cache.get("Ada"));
console.log(calls.length);const calls = [];
function getOrInsertComputed(map, key, callback) {
if (!map.has(key)) map.set(key, callback(key));
return map.get(key);
}
const cache = new Map();
getOrInsertComputed(cache, "Ada", name => { calls.push(name); return name.length; });
getOrInsertComputed(cache, "Ada", name => { calls.push(name); return 99; });
console.log(cache.get("Ada"));
console.log(calls.length);The first call finds no "Ada", so it runs the callback. The callback adds "Ada" to calls and returns 3, the length of the name. The second call finds the key already there, so it returns 3 and skips the callback. So it prints 3, then 1.
Check your understanding
7 QUESTIONSQuestion 1 of 7Why is
new Map([[1, "a"], ["1", "b"]]).sizeequal to 2?Choose an answer to see the explanation.
Question 2 of 7What does this Set with two NaN values print?
Read the code, then predictconst values = new Set([NaN, NaN, 1, 1]); console.log(values.size);Choose an answer to see the explanation.
Question 3 of 7In what order does
map.forEachpass the key and the value to your function?Choose an answer to see the explanation.
Question 4 of 7What does this Set of objects print?
Read the code, then predictconst a = {}; const b = {}; const set = new Set([a, b, a]); console.log(set.size);Choose an answer to see the explanation.
Question 5 of 7What do Object.keys(map).length and map.size print?
Read the code, then predictconst map = new Map([["a", 1], ["b", 2]]); console.log(Object.keys(map).length); console.log(map.size);Choose an answer to see the explanation.
Question 6 of 7When should
getOrInsertComputed(map, key, callback)callcallback?Choose an answer to see the explanation.
Question 7 of 7Which one turns a Map with string keys into a plain object?
Choose an answer to see the explanation.
Key takeaways
- Use a Map for key-value pairs that you add and change, especially when the keys are not strings.
- Use a Set to keep each value once and to ask "is it in here?" Spread it into an array when you need one.
- Maps and Sets both keep the order you added things, and both work with
for...of. getOrInsertreturns the stored value, or adds the value when the key is missing.getOrInsertComputedcalls its callback only when the key is missing.- Turn a Map into an object or an array on purpose before you save it as JSON.
One-liner: a Map answers "what value goes with this key?", a Set answers "have I seen this value?", and an object answers "what named fields describe this one thing?"
Up next: Set operations.