Map and Set: Beyond Objects & Arrays
Stop defaulting to objects and arrays — learn when ES6 Map and Set are the smarter, faster choice for JavaScript developers.

👋 Hey, I'm Mohd Kaif – a student documenting my journey through code. I write about what I'm learning in real-time – the wins, the struggles, and the "aha!" moments. From JavaScript and React to backend systems with Node.js, databases, DevOps, TypeScript, and AI integrations. This blog is my public learning journal: honest, evolving, and always exploring. If you're curious about any of these topics, let's learn and build together!
Ever noticed how a problem that should take five minutes ends up taking an hour — not because the logic is hard, but because the data structure you chose is fighting you the whole way?
Have you ever tried to use an object as a key inside another object, only to get
[object Object]staring back at you?Have you ever written
array.includes()inside a loop and later wondered why your app slows down at scale?Have you ever de-duplicated an array with a chain of filters, only to feel like there has to be a cleaner way?
Have you ever lost track of insertion order in an object and spent time debugging something that shouldn't even be a bug?
If any of those hit close to home — you're not doing it wrong. The problem isn't your skills. The problem is that objects and arrays, as flexible as they are, were never designed to solve every problem. Two data structures that were — Map and Set — often get skipped over because they feel unfamiliar.
This article changes that.
✅ What You'll Learn
What
Mapis and why it's a smarter key-value store than plain objectsWhat
Setis and how it enforces uniqueness without any extra codeHow
Mapdiffers fromObject— with side-by-side comparisons and real codeHow
Setdiffers fromArray— and when the performance difference actually mattersWhen to use each with concrete, real-world scenarios
Common mistakes that trip up even experienced developers — and how to avoid them
No prerequisites beyond basic JavaScript knowledge. If you've written an object or an array before, you're ready.
The Problem with Plain Objects and Arrays
Before diving into Map and Set, it's worth understanding exactly why you'd need them. Most developers know objects and arrays intuitively — but they also carry some hidden gotchas.
What objects struggle with
const cache = {};
const requestKey = { endpoint: '/api/users', params: { page: 1 } };
cache[requestKey] = { data: [...] };
console.log(Object.keys(cache)); // [ '[object Object]' ] 😬
When you use a non-string value as an object key, JavaScript silently coerces it into a string. Your carefully structured object key becomes "[object Object]" — and every subsequent key collides with it.
Other limitations of plain objects:
No built-in
sizeproperty — you have to doObject.keys(obj).lengthevery timePrototype pollution risk — properties like
toStringorconstructorcan interfere unexpectedlyInsertion order — historically unreliable, and still not guaranteed for integer-like keys
What arrays struggle with
const visited = [];
function markVisited(nodeId) {
if (!visited.includes(nodeId)) { // O(n) — gets slower as list grows
visited.push(nodeId);
}
}
Array.includes() scans every element until it finds a match. For small arrays this is invisible. For thousands of items — or when called thousands of times — it becomes a real bottleneck.
Arrays also have no built-in way to enforce uniqueness. Preventing duplicates always requires extra logic on your end.
What is a Map?
A Map is a collection of key-value pairs — like an object — but with one critical difference: keys can be any data type. Objects, functions, numbers, booleans — all valid as keys.
💡 Mental model: Think of a
Maplike a filing cabinet where the label on each folder can be anything — a name, a number, an ID badge, even another folder.
Basic usage
const userMap = new Map();
userMap.set('name', 'Rahul'); // string key
userMap.set(1, 'ID Number'); // number key
userMap.set(true, 'isActive'); // boolean key
console.log(userMap.get('name')); // 'Rahul'
console.log(userMap.size); // 3
The Map API at a glance
| Method / Property | What it does |
|---|---|
map.set(key, value) |
Add or update an entry |
map.get(key) |
Retrieve a value by key |
map.has(key) |
Check if a key exists (true/false) |
map.delete(key) |
Remove a specific entry |
map.clear() |
Remove all entries |
map.size |
Total number of entries |
Using objects as keys — something plain objects simply can't do
const userPermissions = new Map();
const alice = { id: 1, name: 'Alice' };
const bob = { id: 2, name: 'Bob' };
userPermissions.set(alice, ['read', 'write']);
userPermissions.set(bob, ['read']);
console.log(userPermissions.get(alice)); // ['read', 'write']
console.log(userPermissions.get(bob)); // ['read']
With a plain object, both alice and bob would collapse to the same "[object Object]" key. With Map, each object reference is its own unique key.
Clean, predictable iteration
const config = new Map([
['theme', 'dark'],
['language', 'en'],
['timezone', 'UTC+5:30'],
]);
for (const [key, value] of config) {
console.log(`\({key}: \){value}`);
}
// theme: dark
// language: en
// timezone: UTC+5:30
Insertion order is always preserved. No surprises.
🛠️ Practice Challenge #1
Try building a simple request cache using Map. Write a function fetchWithCache(url) that:
Checks if the URL already exists in the Map
Returns the cached result if it does
Simulates a fetch (use
Promise.resolve('data for ' + url)) and stores the result if it doesn't
const cache = new Map();
async function fetchWithCache(url) {
// Your code here
}
Hint: map.has() and map.set() are your friends.
What is a Set?
A Set is a collection of unique values. Add the same value twice and it simply ignores the second one. No error, no duplicate — it just doesn't allow it.
💡 Mental model: Think of a
Setlike a VIP guest list. The bouncer checks IDs — if you're already on the list, you don't get added again.
Basic usage
const tags = new Set();
tags.add('javascript');
tags.add('webdev');
tags.add('javascript'); // duplicate — silently ignored
console.log(tags); // Set { 'javascript', 'webdev' }
console.log(tags.size); // 2
The Set API at a glance
| Method / Property | What it does |
|---|---|
set.add(value) |
Add a value (ignored if already present) |
set.has(value) |
Check if value exists — O(1) |
set.delete(value) |
Remove a specific value |
set.clear() |
Remove all values |
set.size |
Total number of unique values |
The single most common use case: removing duplicates
const rawIds = [1, 2, 2, 3, 4, 4, 4, 5];
const uniqueIds = [...new Set(rawIds)];
console.log(uniqueIds); // [1, 2, 3, 4, 5]
One line. No filter logic. No reduce. This is idiomatic JavaScript.
Fast membership checks
// Array approach — O(n), slows down at scale
const blockedUsers = [101, 204, 389, 512, ...thousandsMore];
if (blockedUsers.includes(userId)) { /* ... */ }
// Set approach — O(1), always instant
const blockedSet = new Set([101, 204, 389, 512, ...thousandsMore]);
if (blockedSet.has(userId)) { /* ... */ }
The difference is invisible with 10 items. With 100,000 items, it's the difference between milliseconds and seconds.
🛠️ Practice Challenge #2
You're building a tag input component. Write a function addTag(tags, newTag) where tags is a Set and newTag is a string. The function should:
Reject the tag if it already exists, returning a message:
"Tag already exists"Reject the tag if it's empty, returning:
"Tag cannot be empty"Add the tag and return:
"Tag added successfully"
function addTag(tags, newTag) {
// Your code here
}
const myTags = new Set(['javascript', 'react']);
console.log(addTag(myTags, 'react')); // "Tag already exists"
console.log(addTag(myTags, '')); // "Tag cannot be empty"
console.log(addTag(myTags, 'nodejs')); // "Tag added successfully"
Map vs Object: The Full Picture
Here's a side-by-side comparison to make the choice clearer:
| Feature | Object | Map |
|---|---|---|
| Key types | Strings & Symbols only | Any type — objects, functions, primitives |
| Insertion order | Not guaranteed for all key types | Always preserved |
| Size | Object.keys(obj).length — manual |
map.size — built-in |
| Default keys | Inherits prototype keys (toString, etc.) |
Starts completely empty |
| Iteration | Requires Object.entries() |
Directly iterable with for...of |
| Performance | Fine for static, simple data | Optimised for frequent reads/writes |
| JSON support | Native JSON.stringify |
Requires manual conversion |
When to choose Object
Simple, static configuration data
When JSON serialization is a must
When you're working with known, string-based keys
When to choose Map
Keys are dynamic, non-string, or object references
Frequent additions and deletions
You need reliable insertion-order iteration
Working with large datasets where performance matters
Set vs Array: The Full Picture
| Feature | Array | Set |
|---|---|---|
| Duplicates | Allowed | Not allowed |
| Membership check | includes() — O(n) |
has() — O(1) |
| Insertion order | Preserved | Preserved |
| Index access | arr[0] |
Not supported |
| Primary use | Ordered sequences | Unique collections |
When to choose Array
Order matters and you need index-based access
Duplicates are valid and expected
You need array-specific methods like
map(),filter(),reduce()
When to choose Set
You need uniqueness guaranteed at the data structure level
Fast membership checks matter
Deduplicating data from an external source
How They Work Under the Hood
Map internals
Map uses a hash table internally, similar to how databases index data. Each key is hashed to a bucket location, making insert, delete, and lookup operations close to O(1) — constant time, regardless of size. It also maintains an internal linked list to preserve insertion order.
Set internals
Set behaves internally like a Map<value, true> — a map where each value is its own key. The same hashing mechanism applies, which is why set.has() is O(1) while array.includes() is O(n).
⚠️ Important gotcha: Sets use reference equality for objects — not value equality.
const set = new Set();
set.add({ role: 'admin' });
set.add({ role: 'admin' }); // Different object reference!
console.log(set.size); // 2 — not 1
Both objects look identical, but they're separate instances in memory. If you need value-based deduplication for objects, you'll need to serialize them first (e.g., JSON.stringify).
Common Mistakes to Avoid
❌ Using an object when you need a non-string key
// This silently breaks
const meta = {};
const domNode = document.querySelector('#app');
meta[domNode] = { clicks: 0 }; // key becomes "[object HTMLDivElement]"
// This works correctly
const meta = new Map();
meta.set(domNode, { clicks: 0 });
❌ Expecting Set to deduplicate objects by content
As shown above — Sets compare objects by reference. { a: 1 } and { a: 1 } are two different objects.
❌ Using array.includes() inside a performance-sensitive loop
// ❌ O(n²) — painful at scale
const seen = [];
for (const item of largeDataset) {
if (!seen.includes(item.id)) {
seen.push(item.id);
process(item);
}
}
// ✅ O(n) — clean and fast
const seen = new Set();
for (const item of largeDataset) {
if (!seen.has(item.id)) {
seen.add(item.id);
process(item);
}
}
❌ Treating Set like a Map
const set = new Set();
set.add({ key: 'theme', value: 'dark' }); // This is just a value
set.get('theme'); // ❌ — Set has no .get()
If you need key-value pairs, that's Map's job.
🛠️ Practice Challenge #3
You're processing a stream of user events. Some events arrive more than once (network duplicates). Write a function processEvents(events) that:
Skips any event whose
idhas already been processedLogs
Processing event: [id]for each new eventReturns the count of unique events processed
function processEvents(events) {
// Your code here
}
const events = [
{ id: 'e1', type: 'click' },
{ id: 'e2', type: 'scroll' },
{ id: 'e1', type: 'click' }, // duplicate
{ id: 'e3', type: 'keydown' },
];
console.log(processEvents(events)); // 3
Real-World Scenarios Where This Matters
Scenario 1: Caching API responses with Map
const apiCache = new Map();
async function getUser(userId) {
if (apiCache.has(userId)) {
console.log('Cache hit');
return apiCache.get(userId);
}
const user = await fetch(`/api/users/${userId}`).then(r => r.json());
apiCache.set(userId, user);
return user;
}
Using a plain object here would work for string IDs — but Map scales more safely as your key types evolve.
Scenario 2: Tracking visited nodes in a graph traversal
function bfs(graph, start) {
const visited = new Set();
const queue = [start];
while (queue.length > 0) {
const node = queue.shift();
if (visited.has(node)) continue; // O(1) check
visited.add(node);
console.log('Visiting:', node);
queue.push(...graph[node]);
}
}
An array would work here — but every includes() check scans the whole list. A Set makes this clean and efficient.
Scenario 3: Storing metadata about DOM elements
const elementMeta = new Map();
document.querySelectorAll('.card').forEach(card => {
elementMeta.set(card, {
clickCount: 0,
lastInteracted: null,
});
});
document.addEventListener('click', (e) => {
const card = e.target.closest('.card');
if (card && elementMeta.has(card)) {
const meta = elementMeta.get(card);
meta.clickCount++;
meta.lastInteracted = Date.now();
}
});
This pattern is impossible with plain objects — DOM nodes can't be string keys.
Quick Decision Guide
Use Map when:
Your keys are not strings
You need to iterate key-value pairs in insertion order
You're building a cache, a registry, or a lookup table
You're adding and removing keys frequently
Use Set when:
You need uniqueness guaranteed automatically
You're deduplicating data from an external source
You need O(1) membership checks (visited nodes, blocked IDs, active sessions)
You're tracking a collection where order matters but duplicates don't
Stick with Object when:
Data is simple and static
You need JSON serialisation
Keys are fixed, known strings
Stick with Array when:
You need index-based access
Order matters and duplicates are valid
You're chaining
.map(),.filter(), or.reduce()
What to Learn Next
Now that Map and Set are in your toolkit, here's a natural path forward:
WeakMapandWeakSet— Garbage-collector-friendly versions ofMapandSet. Perfect for storing metadata about objects without preventing them from being garbage collected. A must-know for advanced memory management.JavaScript Iterators and Generators —
MapandSetare iterable, and understanding the iterator protocol will help you write custom data structures with the same ergonomics.Data Structures and Algorithms in JS — Now that you know when O(1) vs O(n) matters, explore stacks, queues, linked lists, and trees — all implementable using the primitives you already know.
Performance Profiling with Chrome DevTools — Learn to measure the actual performance difference between your data structure choices in real applications, not just in theory.
💬 Got Questions?
Drop a comment below! I'd love to hear how you're using (or planning to use) Map and Set in your own projects — or if something in this article tripped you up, let's work through it together.
Here are some topics I'm covering in upcoming articles:
WeakMapandWeakSetExplained: Why they exist and when to reach for them over regularMapandSetJavaScript Memory Management: How the garbage collector works and how your data structure choices affect it
Big-O Notation for JS Developers: A practical guide to time and space complexity without the computer science jargon
Writing Your Own Data Structures in JavaScript: Build a linked list, stack, and queue from scratch — and understand why you'd ever want to
Found this helpful? Share it with a fellow developer who's still reaching for objects and arrays by default. And if you want more content like this, follow along — there's a lot more coming.




