Map and Set in JavaScript: Better Data Structures for the Right Problems
JavaScript has always had objects for key-value storage and arrays for ordered collections. They are versatile and familiar, but they come with real limitations that bite you in specific situations. Map and Set — introduced in ES6 — were designed to fill exactly those gaps.
This article covers what Map and Set are, how they differ from their traditional counterparts, and the specific situations where reaching for them produces cleaner, more correct code.
The Problems with Traditional Objects and Arrays
Before looking at what Map and Set offer, it helps to understand the pain points they address.
Objects as key-value stores have hidden constraints
const store = {};
store["name"] = "Priya";
store[1] = "one";
store[true] = "bool value";
console.log(Object.keys(store));
// ["1", "name", "true"]
Every key in an object is coerced to a string. The number 1 becomes "1". The boolean true becomes "true". If you need non-string keys — objects, functions, DOM nodes — a plain object cannot do it. You lose the original type completely.
Objects also carry inherited properties from Object.prototype:
const counts = {};
console.log("toString" in counts); // true — inherited, not yours
console.log("constructor" in counts); // true
If someone passes a key that happens to collide with a prototype property, you get unexpected behaviour. Getting the size of an object requires Object.keys(obj).length — there is no built-in .size property. And the insertion order of keys was not guaranteed until ES2015, and even then only for string keys.
Arrays allow duplicates and linear searches
const tags = ["javascript", "web", "javascript", "es6"];
console.log(tags.length); // 4 — duplicate exists
If you use an array as a collection of unique items, you have to enforce uniqueness manually. Checking whether a value exists requires .includes(), which is O(n) — it scans every element. For small arrays this is fine, but for large ones it becomes a bottleneck.
Map and Set exist to solve these specific problems cleanly.
What Is Map?
A Map is a collection of key-value pairs where the keys can be of any type — including objects, functions, and primitives. It remembers the insertion order of its entries and provides direct methods for getting, setting, checking, and deleting entries.
const map = new Map();
map.set("name", "Priya");
map.set(42, "the answer");
map.set(true, "boolean key");
const objKey = { id: 1 };
map.set(objKey, "object as key");
console.log(map.get("name")); // "Priya"
console.log(map.get(42)); // "the answer"
console.log(map.get(objKey)); // "object as key"
console.log(map.size); // 4
No type coercion. 42 stays 42 as a key, not "42". An object reference used as a key maps to exactly that object — no collision with string keys, no prototype interference.
Creating a Map
You can initialize a Map from an array of [key, value] pairs:
const user = new Map([
["name", "Rahul"],
["age", 34],
["city", "Bengaluru"]
]);
console.log(user.get("name")); // "Rahul"
console.log(user.size); // 3
Core Map methods
const m = new Map();
m.set("key", "value"); // add or update
m.get("key"); // "value"
m.has("key"); // true
m.delete("key"); // removes the entry, returns true
m.clear(); // removes all entries
m.size; // number of entries (not a method — a property)
Iterating over a Map
Maps maintain insertion order, and they are directly iterable:
const scores = new Map([
["Priya", 95],
["Rahul", 88],
["Ananya", 92]
]);
for (const [name, score] of scores) {
console.log(`\({name}: \){score}`);
}
// Priya: 95
// Rahul: 88
// Ananya: 92
scores.forEach((score, name) => {
console.log(`\({name}: \){score}`);
});
console.log([...scores.keys()]); // ["Priya", "Rahul", "Ananya"]
console.log([...scores.values()]); // [95, 88, 92]
console.log([...scores.entries()]); // [["Priya", 95], ...]
Note the argument order in forEach: the value comes first, then the key. This matches the convention of other iterator callbacks (value, key) rather than the more intuitive key, value. It is a minor quirk worth remembering.
What Is Set?
A Set is a collection of unique values. Any value you add that already exists in the Set is silently ignored. Like Map, it maintains insertion order and accepts values of any type.
const set = new Set();
set.add(1);
set.add(2);
set.add(2); // duplicate — ignored
set.add(3);
set.add("hello");
console.log(set.size); // 4
console.log(set.has(2)); // true
console.log(set.has(5)); // false
The duplicate 2 does not throw an error — it is simply not added. The Set always contains only distinct values.
Creating a Set
You can initialize a Set from any iterable:
const nums = new Set([1, 2, 3, 2, 1]);
console.log(nums.size); // 3 — duplicates removed
const letters = new Set("hello");
console.log([...letters]); // ["h", "e", "l", "o"] — duplicate "l" removed
Core Set methods
const s = new Set();
s.add("value"); // add a value
s.has("value"); // true
s.delete("value"); // removes, returns true
s.clear(); // removes all values
s.size; // number of values
Iterating over a Set
const fruits = new Set(["mango", "banana", "guava", "mango"]);
for (const fruit of fruits) {
console.log(fruit);
}
// mango
// banana
// guava
fruits.forEach(fruit => console.log(fruit));
console.log([...fruits]); // ["mango", "banana", "guava"]
Sets are iterable, so spread and for...of work directly. The order is the order of first insertion.
Map vs Object: Key Differences
| Object | Map | |
|---|---|---|
| Key types | Strings and Symbols only (others coerced to string) | Any value — objects, functions, primitives |
| Prototype keys | Inherits keys from Object.prototype |
No inherited keys |
| Size | Object.keys(obj).length |
map.size |
| Iteration order | Not guaranteed for all key types | Always insertion order |
| Performance | Good for small, static key sets | Better for frequent additions and deletions |
| Serialization | JSON.stringify works natively |
Requires manual conversion |
When the difference in key type matters
// Object — DOM node as key fails
const cache = {};
const element = document.querySelector("#btn");
cache[element] = "clicked"; // key becomes "[object HTMLButtonElement]"
// Map — DOM node as key works perfectly
const cache = new Map();
cache.set(element, "clicked"); // key is the exact element reference
cache.get(element); // "clicked"
Caching computed values keyed by DOM elements, request objects, or any non-string value — this is exactly where Map shines and plain objects fail.
When object size matters
const obj = { a: 1, b: 2, c: 3 };
const size = Object.keys(obj).length; // 3 — extra work required
const map = new Map([["a", 1], ["b", 2], ["c", 3]]);
const size = map.size; // 3 — directly available
For frequently-counted collections, map.size is meaningfully more convenient.
Prototype key collision — a real bug
const wordCounts = {};
const words = ["constructor", "toString", "hasOwnProperty"];
words.forEach(word => {
wordCounts[word] = (wordCounts[word] || 0) + 1;
});
console.log(wordCounts["toString"]); // 1 — works, but shadowing prototype methods
console.log("toString" in wordCounts); // true — ambiguous
Using Map eliminates any possibility of collision:
const wordCounts = new Map();
words.forEach(word => {
wordCounts.set(word, (wordCounts.get(word) || 0) + 1);
});
// No prototype interference — ever
Set vs Array: Key Differences
| Array | Set | |
|---|---|---|
| Uniqueness | Allows duplicates | Only unique values |
| Membership check | arr.includes(val) — O(n) |
set.has(val) — O(1) |
| Removing duplicates | Manual filtering required | Automatic |
| Index access | arr[0], arr[1], etc. |
Not supported |
| Built-in set operations | None | Manual (union, intersection, difference) |
| Order | Insertion order preserved | Insertion order preserved |
The membership check performance gap
For large collections, the difference between O(n) and O(1) membership testing is significant:
const items = Array.from({ length: 100000 }, (_, i) => i);
const itemArray = items;
const itemSet = new Set(items);
// Array: scans up to 100,000 elements
itemArray.includes(99999); // O(n)
// Set: hash-based lookup
itemSet.has(99999); // O(1)
For a collection of ten items this does not matter. For a collection of a hundred thousand, it does.
Deduplicating an array
const tags = ["js", "web", "js", "es6", "web", "node"];
// Array — manual deduplication
const unique = tags.filter((tag, index) => tags.indexOf(tag) === index);
// Set — automatic
const unique = [...new Set(tags)];
console.log(unique); // ["js", "web", "es6", "node"]
The Set approach is two characters of meaningful syntax (new Set(...)) wrapped in spread. The array approach requires understanding that indexOf returns the first occurrence index, and filtering based on that.
Set operations (union, intersection, difference)
Arrays have no built-in set math. Sets make it straightforward:
const a = new Set([1, 2, 3, 4]);
const b = new Set([3, 4, 5, 6]);
// Union — all values from both sets
const union = new Set([...a, ...b]);
console.log([...union]); // [1, 2, 3, 4, 5, 6]
// Intersection — values in both sets
const intersection = new Set([...a].filter(x => b.has(x)));
console.log([...intersection]); // [3, 4]
// Difference — values in a but not in b
const difference = new Set([...a].filter(x => !b.has(x)));
console.log([...difference]); // [1, 2]
The intersection and difference patterns use the fact that set.has() is O(1) — making these operations O(n) rather than O(n²) if you were doing the same thing with arrays.
When to Use Map and Set
Use Map when:
Keys are not strings. If you need to key a collection by objects, DOM nodes, functions, or any non-string value, Map is the correct tool. Objects cannot do this correctly.
// Caching expensive computations by their input object
const resultCache = new Map();
function expensiveCalc(input) {
if (resultCache.has(input)) return resultCache.get(input);
const result = /* ... compute ... */ input.value * 2;
resultCache.set(input, result);
return result;
}
You need the size directly. If you frequently check how many entries a collection has, map.size is simpler than Object.keys(obj).length.
Insertion order matters and keys are dynamic. Maps always iterate in insertion order, with no surprises from inherited properties or numeric key reordering.
Frequent additions and deletions. Maps are optimised for dynamic key-value storage. Objects are optimised for a fixed set of known string keys (like a record or config object).
Use Set when:
You need a collection of unique values. Any time "this collection should not have duplicates" is a requirement, Set is the right structure. It enforces uniqueness for you.
// Track which user IDs have been processed
const processedIds = new Set();
function processUser(id) {
if (processedIds.has(id)) return; // already done
processedIds.add(id);
// ... do the work
}
You need fast membership testing. If you are checking if (collection includes X) frequently, Set.has() is O(1) vs Array.includes() O(n).
// A permission system — fast role checking
const allowedRoles = new Set(["admin", "editor", "moderator"]);
function canEdit(userRole) {
return allowedRoles.has(userRole); // O(1)
}
Deduplicating input. Whenever you receive data that might have duplicates and you need only the distinct values, converting through a Set is the idiomatic approach.
function uniqueSorted(arr) {
return [...new Set(arr)].sort();
}
uniqueSorted([3, 1, 2, 1, 3, 2]); // [1, 2, 3]
Keep using Object when:
Keys are known string names (a user record, a config object, a response body)
You need
JSON.stringifyorJSON.parseto work directlyYou are defining a data shape with fixed fields
Keep using Array when:
Order matters and duplicates are valid
You need index-based access (
arr[0])You need the full array method toolkit (
map,filter,reduce,sort,slice)
Converting Between Structures
// Map → Object
const map = new Map([["a", 1], ["b", 2]]);
const obj = Object.fromEntries(map);
// { a: 1, b: 2 }
// Object → Map
const obj = { a: 1, b: 2 };
const map = new Map(Object.entries(obj));
// Set → Array
const set = new Set([1, 2, 3]);
const arr = [...set]; // or Array.from(set)
// Array → Set (deduplication)
const arr = [1, 2, 2, 3];
const set = new Set(arr);
Object.fromEntries and Object.entries are the bridge between objects and maps. Spread and Array.from are the bridge between sets and arrays.
Wrapping Up
Map and Set are not replacements for objects and arrays — they are the right tools for problems that objects and arrays handle awkwardly.
Reach for Map when your keys are not strings, when you need an accurate .size, or when you need a clean key-value store with no prototype interference. Reach for Set when uniqueness matters, when you need O(1) membership testing, or when you are working with union, intersection, or difference operations.
Understanding which data structure fits the problem is a fundamental skill. In many situations, Map and Set are the precise fit that produces correct, readable, and efficient code where a plain object or array would require workarounds.
Comments
Join the discussionNo comments yet. Be the first to comment.
