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 collectionCompare Maps with plain objects and Sets with arrays.
- 02Use real iteration patternsLoop with entries, keys, values, destructuring, and
forEachsafely. - 03Upsert without double workFeature-detect upcoming
getOrInsertmethods and use reference helpers when needed.
Maps, Sets, and phone contacts
JavaScript already has plain objects and arrays, so why learn two more collections? Because some jobs need clearer rules. A Map is a keyed collection whose keys can be any value. A Set is a collection of values where each value can appear only once.
You have built on object basics, object references, comparisons, iterating arrays, and destructuring. This lesson uses those ideas to model real data: counters, caches, tags, and lookups where the key is not always a string.
Think of a Map as a phone contact list. You use a saved key to find one detail. JavaScript lets that key be a string, number, object, or another value.
- In real life: A contact can use any saved key
- In JavaScript: A Map key can be any JavaScript value
- In real life: Each key finds one contact detail
- In JavaScript: Each key points to one value
- In real life: New contacts keep their added order
- In JavaScript: Map iteration follows insertion order
Where the analogy stops: A phone contact list uses names and numbers. Map also allows object keys, which match only when they are the same object.
A Set is the bouncer’s list: if Ada is already on it, adding Ada again does not create a second Ada. That makes Sets perfect when membership and uniqueness matter more than positions.
- In real life: Writing a name twice still means one guest
- In JavaScript: Adding the same value twice keeps one Set entry
- In real life: You can ask if a name is on the list
- In JavaScript: Use
set.has(value) - In real life: The first time a guest was added is remembered
- In JavaScript: Set iteration preserves insertion order
Where the analogy stops: A guest list may merge two people with the same name. A Set only merges values JavaScript considers the same. Two separate {} objects both stay.
A Map stores key-value entries with any key type. A Set stores unique values. Both are iterable and preserve insertion order.
Map vs plain objects
INTERACTIVEA plain object is still the best shape for many records: a user profile, a product returned from an API, or settings you plan to save as JSON. But object property keys are strings or symbols. If you write obj[1], the property name is really "1". If you write obj[], the property name is usually "[object Object]".
A Map does not do that conversion. It keeps the number 1, the string "1", an object, and NaN as distinct keys. It also avoids inherited names: "toString" is not accidentally one of your Map entries.
Predict which keys collide in the object, 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"]]falseMap keeps 1, "1", the object, and NaN as four distinct keys.
| Plain object | Map | |
|---|---|---|
| Key types | Strings and symbols only; other keys are converted | Any value: objects, functions, numbers, strings, NaN, and more |
| Order | Own property order follows object property rules | Iteration is insertion order |
| Size | Use Object.keys(obj).length for enumerable string keys | Use map.size |
| Iteration | Use Object.keys, values, entries, or for...in carefully | Directly iterable with for...of, .keys(), .values(), .entries() |
| JSON | Natural with JSON.stringify(obj) | Convert first; Maps do not serialize entries directly |
| Frequent add/delete | Fine for records, less clear for dynamic dictionaries | Designed for dynamic keyed collections |
Use an object for a fixed record with known property names. Reach for Map when entries are added and removed often, when you need non-string keys, or when the thing really is a lookup table rather than a record.
Set for uniqueness
STEP THROUGHA Set answers one question very well: “Have I seen this value already?” Creating a Set from an array removes duplicates while keeping the first time each value appeared: [...new Set(array)] is a common dedupe pattern.
Step through a Set and watch duplicates disappear without sorting the values.
script
const unique = new Set(list);unique.add(3).add(4);console.log(unique.size);console.log([...unique]);Set uses the SameValueZero comparison you saw near Comparisons: NaN equals NaN here, and 0 and -0 count as the same value. Objects still compare by reference. Two separate object literals are two different values, even if both print as .
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"]Iterating Map & Set
STEP THROUGHMap and Set are iterable, which means for...of, spread, and destructuring work naturally. For a Map, the default iterator yields entries, so this destructuring shape is common: for (const [key, value] of map).
Follow the order: Map entries, Map forEach's value-key callback, then a Set made from 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(", "));Useful conversions go both ways. new Map(Object.entries(obj)) turns object entries into a Map. Object.fromEntries(map) turns a Map with property-friendly keys back into an object. [...set] and [...map.keys()] create arrays when array methods are more convenient.
Map forEach passes (value, key), not (key, value). In for...of, destructuring an entry uses [key, value]. The two forms look similar, so say the order out loud while learning.
getOrInsert & getOrInsertComputed (upsert)
INTERACTIVE“Upsert” means “give me the existing entry, or insert one if it is missing.” The Map upsert proposal adds Map.prototype.getOrInsert and Map.prototype.getOrInsertComputed. They are a recent addition to the language that browsers are rolling out, so this lesson feature-detects them after the page mounts and uses a small reference helper when they are not available.
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"]Checking support after mount avoids a server/browser hydration mismatch.
The semantics are the important part: getOrInsert(map, key, value) sets value only when key is missing, then returns map.get(key). getOrInsertComputed(map, key, callback) calls the callback only when the key is missing, and it passes the key to the callback. Existing entries are returned without recomputing.
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=5Where you’ll use this
INTERACTIVEMaps and Sets are everyday tools for shaping data before rendering it. A search page might keep a Set of selected filter IDs. An analytics panel might count events in a Map. A component library might cache measurements by object reference. A form might convert a Map to an object before saving.
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 unique word keys. Repeated words update the existing count instead of adding another entry.
- Word counters: Map from word to count, preserving first-seen order.
- De-duplication: Set from an array, then spread back to an array.
- Object-keyed caches: Map while values should stay strongly reachable; WeakMap & WeakSet comes later.
- Serialization boundary: convert with entries before JSON. The JSON lesson explains why Maps do not serialize directly.
Map, Set, or plain object?
SORTChoosing the structure is usually about the question you need to ask: “what fields describe this thing?”, “what value belongs to this key?”, or “have I seen this value already?” Sort the scenarios below.
- A JSON-shaped user record with fixed fields
- Cache data by a DOM node or other object
- Keep a list of tags without duplicates
- Count words as
word → countpairs - App settings loaded and saved as JSON
- Remember which product IDs have appeared
Sort each scenario into the structure that fits best.
Common misconceptions
- “Map is just a fancier object.” Map entries are not properties; non-string keys stay intact and
Object.keys(map)does not list entries. - “Set compares objects by shape.” It compares object references. Two
literals are different values. - “Set sorts things.” Set removes duplicates but preserves insertion order; it does not sort.
- “JSON.stringify(new Map()) saves entries.” It does not. Convert first with arrays or
Object.fromEntrieswhen keys are strings. - “getOrInsertComputed always runs the callback.” It runs only for missing keys. That is the whole cache-friendly point.
Practice exercises
5 EXERCISESPredict the printed result.
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. Later duplicates are ignored, so the joined output is 7,2,9.
Run the counter mentally and enter the two console 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 is seen twice, so counts.get("Ada") is 2. The Map has two keys: Ada and Lin.
What exact JSON string 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 entries become object properties, then JSON prints the plain object as {"theme":"dark","page":"home"}.
Predict map.size after the inserts.
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 number 1, string "1", the object reference, and NaN. The second NaN overwrites the first NaN value, so size is 4.
Use the helper semantics to predict the two console 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 stores 3 and records one callback call. The second call sees the key already exists, returns 3, and does not call the callback again.
Check your understanding
7 QUESTIONSQuestion 1 of 7Why does
new Map([[1, "a"], ["1", "b"]]).sizeequal 2?Choose an answer to see the explanation.
Question 2 of 7What does the Set with duplicate 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 7What is the callback argument order for
map.forEach?Choose an answer to see the explanation.
Question 4 of 7What does the Set of object references 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 conversion turns a Map with string keys into a plain object?
Choose an answer to see the explanation.
Key takeaways
- Use Map for dynamic key-value entries, especially when keys are not strings.
- Use Set for membership and uniqueness; spread it back to an array when needed.
- Both Map and Set preserve insertion order and work with
for...of. - Upsert helpers return the existing value or insert a missing one; computed upsert calls its callback only when missing.
- Convert Maps intentionally at boundaries such as objects, arrays, and JSON.
One-liner: Map answers “what value belongs to this key?”, Set answers “have I seen this value?”, and objects answer “what named fields describe this record?”
Up next: Set operations.