Skip to main content

Command Palette

Search for a command to run...

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.

Updated
Map and Set: Beyond Objects & Arrays
M

👋 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 Map is and why it's a smarter key-value store than plain objects

  • What Set is and how it enforces uniqueness without any extra code

  • How Map differs from Object — with side-by-side comparisons and real code

  • How Set differs from Array — and when the performance difference actually matters

  • When 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 size property — you have to do Object.keys(obj).length every time

  • Prototype pollution risk — properties like toString or constructor can interfere unexpectedly

  • Insertion 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 Map like 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:

  1. Checks if the URL already exists in the Map

  2. Returns the cached result if it does

  3. 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 Set like 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:

  1. Reject the tag if it already exists, returning a message: "Tag already exists"

  2. Reject the tag if it's empty, returning: "Tag cannot be empty"

  3. 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:

  1. Skips any event whose id has already been processed

  2. Logs Processing event: [id] for each new event

  3. Returns 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:

  1. WeakMap and WeakSet — Garbage-collector-friendly versions of Map and Set. Perfect for storing metadata about objects without preventing them from being garbage collected. A must-know for advanced memory management.

  2. JavaScript Iterators and GeneratorsMap and Set are iterable, and understanding the iterator protocol will help you write custom data structures with the same ergonomics.

  3. 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.

  4. 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:

  • WeakMap and WeakSet Explained: Why they exist and when to reach for them over regular Map and Set

  • JavaScript 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.

Zero to Full Stack Developer: From Basics to Production

Part 24 of 50

Complete full-stack web development series from zero to production. Learn HTML, CSS, JavaScript, TypeScript, React, Next.js, Node.js, databases, Docker, AWS, and AI integration. Build real-world projects step-by-step.

Up next

Mastering Spread and Rest Operators in JavaScript

Learn the one mental model that separates spread from rest — with practical patterns, real-world exercises, and the pitfalls every JavaScript developer should know.