Map and Set
Use Map for key-value data with any key type, Set for unique values, the Set methods union and intersection, and Object.groupBy to group data.
What you will learn
- Create and use Map and Set and iterate over them
- Choose between a Map and a plain object
- Combine sets with union, intersection and difference
- Group an array with Object.groupBy and Map.groupBy
On this page
A map stores values under keys, and a set stores unique values. Why they exist and what they cost is covered in the fundamentals lesson on maps and sets. JavaScript has built-in Map and Set classes. Plain objects can also act as dictionaries, so this lesson also tells you which to pick.
Map
A Map holds key-value pairs. Build one from an array of [key, value] pairs or start empty and call set.
const stock = new Map([ ["apple", 5], ["pear", 0],]);
stock.set("plum", 12);stock.set("apple", 7);console.log(stock);Map(3) { 'apple' => 7, 'pear' => 0, 'plum' => 12 }Reading, deleting and looping:
console.log( stock.get("apple"), stock.get("kiwi"), stock.has("pear"), stock.size,);
stock.delete("pear");console.log([...stock.keys()], [...stock.values()]);
for (const [fruit, count] of stock) { console.log(fruit, count);}7 undefined true 3[ 'apple', 'plum' ] [ 7, 12 ]apple 7plum 12Keys can be any value, and objects convert to and from maps:
const byObject = new Map();const keyA = { id: 1 };byObject.set(keyA, "first");byObject.set(1, "number").set("1", "string");console.log(byObject.get(keyA), byObject.get({ id: 1 }));console.log(byObject.get(1), byObject.get("1"));
console.log(new Map(Object.entries({ a: 1 })));console.log(Object.fromEntries(new Map([["b", 2]])));first undefinednumber stringMap(1) { 'a' => 1 }{ b: 2 }setreturns the map, so calls chain. Setting an existing key replaces its value and keeps its position.getreturnsundefinedfor a missing key.hasasks whether it exists.deleteremoves a key.sizeis a property, not a method.keys(),values()andentries()return iterators. Spread them into an array to print or index them. Iterating the map itself, as thefor...ofloop does, gives[key, value]pairs.- Keys can be any value: objects, numbers, functions. Keys are compared by identity for objects, so
get({ id: 1 })does not find the entry stored under a different{ id: 1 }. The number1and the string"1"are different keys. new Map(Object.entries(obj))converts an object to a map, andObject.fromEntries(map)converts back.
Set
A Set holds unique values. Adding a value that is already there does nothing.
const seen = new Set([3, 1, 3, 2, 1]);console.log(seen, seen.size);
seen.add(4).add(1);console.log(seen.has(4), seen.has(9));seen.delete(3);console.log([...seen]);
const unique = [...new Set(["a", "b", "a", "c", "b"])];console.log(unique);
console.log(new Set([NaN, NaN, 0, -0]).size);console.log(new Set([{ a: 1 }, { a: 1 }]).size);
seen.forEach((value) => console.log("item", value));Set(3) { 3, 1, 2 } 3true false[ 1, 2, 4 ][ 'a', 'b', 'c' ]22item 1item 2item 4- The constructor accepts any iterable, and drops the duplicates.
[...new Set(array)]is the usual way to remove duplicates from an array. addreturns the set,hastests membership anddeleteremoves.- Equality is “same value zero”:
NaNequalsNaN, and0equals-0. Objects are compared by identity, so two separate{ a: 1 }literals both stay. - Both
MapandSetiterate in insertion order.
Set methods
A Set has methods for the usual set operations. They return new sets and leave the originals alone. The operations were added in ES2025 and run on Node 22.
const ada = new Set(["grace", "linus", "alan"]);const bob = new Set(["alan", "dennis", "grace"]);
console.log(ada.union(bob));console.log(ada.intersection(bob));console.log(ada.difference(bob));console.log(ada.symmetricDifference(bob));
console.log(new Set(["alan"]).isSubsetOf(ada));console.log(ada.isSupersetOf(new Set(["alan", "x"])));console.log(ada.isDisjointFrom(new Set(["zed"])));Set(4) { 'grace', 'linus', 'alan', 'dennis' }Set(2) { 'grace', 'alan' }Set(1) { 'linus' }Set(2) { 'linus', 'dennis' }truefalsetruea.union(b): everything in either.a.intersection(b): only what is in both.a.difference(b): what is inabut not inb.a.symmetricDifference(b): what is in exactly one of them.isSubsetOf,isSupersetOfandisDisjointFromanswer true or false.
Before these existed, you wrote them with filter and has. If your code must run on a Node version older than 22, check that these methods exist first.
Map or object?
const asObject = {};asObject["b"] = 1;asObject["a"] = 2;asObject[10] = "ten";asObject[2] = "two";console.log(Object.keys(asObject));
const asMap = new Map();asMap.set("b", 1).set("a", 2).set(10, "ten").set(2, "two");console.log([...asMap.keys()]);
console.log("toString" in {}, new Map().has("toString"));console.log(Object.keys(asObject).length, asMap.size);
const counts = {};counts[1] = "number one";console.log(counts["1"]);[ '2', '10', 'b', 'a' ][ 'b', 'a', 10, 2 ]true false4 4number one| Plain object | Map |
|
|---|---|---|
| Key types | strings and symbols (numbers become strings) | any value |
| Key order | whole-number keys first, ascending; then insertion order | insertion order |
| Inherited names | "toString" in {} is true |
none: has("toString") is false |
| Size | Object.keys(o).length |
map.size |
| JSON | JSON.stringify works |
not directly |
| Best for | fixed records with known fields | collections that grow, lookups by arbitrary keys, counting |
Use an object for a record with known fields, like a user. Use a Map for a lookup table whose keys come from data, such as a count per word.
Grouping
Object.groupBy(items, callback) sorts items into groups. The callback returns the group key for each item, and the result is an object of arrays. Map.groupBy does the same into a Map, so the keys can be anything. Both were added in ES2024.
const people = [ { name: "Ada", team: "eng" }, { name: "Bob", team: "ops" }, { name: "Cy", team: "eng" },];
const byTeam = Object.groupBy(people, (p) => p.team);console.log(byTeam);console.log(Object.getPrototypeOf(byTeam));
const mapped = Map.groupBy(people, (p) => p.name.length);console.log(mapped);console.log(mapped.get(3).map((p) => p.name));[Object: null prototype] { eng: [ { name: 'Ada', team: 'eng' }, { name: 'Cy', team: 'eng' } ], ops: [ { name: 'Bob', team: 'ops' } ]}nullMap(2) { 3 => [ { name: 'Ada', team: 'eng' }, { name: 'Bob', team: 'ops' } ], 2 => [ { name: 'Cy', team: 'eng' } ]}[ 'Ada', 'Bob' ]The object result has a null prototype, which is why getPrototypeOf prints null and the console shows [Object: null prototype]. It means a group named toString cannot clash with an inherited property. Map.groupBy is the choice when the keys are not strings, as with the name lengths here.
Upserting into a map
A frequent pattern is “get the value for this key, creating it first if it is missing”. Map and WeakMap now have getOrInsert(key, defaultValue) for it Requires Node.js 26 or later, and getOrInsertComputed(key, fn) for when creating the default is costly. They are part of ES2026 and need Node 26. On older versions write if (!map.has(key)) map.set(key, []).
const groups = new Map();
for (const word of ["apple", "avocado", "banana", "blueberry", "cherry"]) { groups.getOrInsert(word[0], []).push(word);}console.log(groups);
const counts = new Map();for (const ch of "hello") { counts.set(ch, counts.getOrInsert(ch, 0) + 1);}console.log(counts);
const lazy = new Map();console.log(lazy.getOrInsertComputed("k", (key) => key.toUpperCase()));Map(3) { 'a' => [ 'apple', 'avocado' ], 'b' => [ 'banana', 'blueberry' ], 'c' => [ 'cherry' ]}Map(4) { 'h' => 1, 'e' => 1, 'l' => 2, 'o' => 1 }KgetOrInsert returns the existing value, or stores the default and returns it. Because the returned array is the one stored in the map, .push(word) updates the map. getOrInsertComputed calls the function with the key only when the key is missing.
Common pitfalls
- Using
map["key"]instead ofmap.get("key"). It sets an ordinary property on the map object and does not store an entry. - Looking up an object key with a look-alike object. Keys compare by identity.
- Printing
map.keys()and expecting an array. It is an iterator. Spread it first. - Using
map.length. The property issize. - Assuming a
Setcomparison works with objects by content. It does not. - Using a plain object with user-supplied keys. Names such as
"toString"can collide with inherited properties. - Expecting
JSON.stringify(map)to list the entries. It gives{}. Convert withObject.fromEntries(map)or[...map]first.
Exercises
Count how often each word appears in "the cat and the hat and the bat" using a Map. Print the words from most to least frequent, with ties in alphabetical order. The output should be the: 3, and: 2, then bat: 1, cat: 1 and hat: 1, one per line.
Show solution
const text = "the cat and the hat and the bat";
const counts = new Map();for (const word of text.split(" ")) { counts.set(word, (counts.get(word) ?? 0) + 1);}
const ranked = [...counts].sort( ([wordA, countA], [wordB, countB]) => countB - countA || wordA.localeCompare(wordB, "en"),);
for (const [word, count] of ranked) { console.log(`${word}: ${count}`);}the: 3and: 2bat: 1cat: 1hat: 1The loop reads the current count with get, uses ?? 0 for a word not seen yet, and stores the new count. [...counts] turns the map into an array of [word, count] pairs. The comparator sorts by count descending and uses localeCompare to break ties. Destructuring in the comparator’s parameters gives the pairs readable names.
Given this record of each person’s friends, find the friends all three people share and the set of everyone mentioned. Print each as a sorted array. The output should be [ 'alan', 'grace' ] and then [ 'alan', 'dennis', 'grace', 'ken', 'linus' ].
const friends = { ada: ["grace", "linus", "alan"], bob: ["alan", "dennis", "grace"], cy: ["grace", "alan", "ken"],};Show solution
const friends = { ada: ["grace", "linus", "alan"], bob: ["alan", "dennis", "grace"], cy: ["grace", "alan", "ken"],};
const sets = Object.values(friends).map((list) => new Set(list));const common = sets.reduce((acc, set) => acc.intersection(set));console.log([...common].sort());
const everyone = sets.reduce((acc, set) => acc.union(set));console.log([...everyone].sort());[ 'alan', 'grace' ][ 'alan', 'dennis', 'grace', 'ken', 'linus' ]Each array becomes a Set. reduce with intersection keeps only the names found in every set, and reduce with union collects every name. The sets are spread into arrays and sorted for stable printing. There is no starting value on the reduce calls, so the first set is the start, which is correct because the list is not empty.
Summary
Mapstores pairs under any kind of key and keeps insertion order. Read withget, test withhas, count withsize.Setstores unique values.[...new Set(items)]removes duplicates.- Set methods
union,intersection,differenceand friends return new sets and run on Node 22. Object.groupByandMap.groupBygroup an array by a key computed per item.getOrInsertonMapis new and needs Node 26.- Use objects for fixed records, and maps for lookup tables built from data.
Next: JSON, the text format for data that objects and arrays map onto.