Skip to main content

Map and Set in JavaScript: Better Data Structures for the Right Problems

Updated 11 min readMarkdown

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.stringify or JSON.parse to work directly

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

No comments yet. Be the first to comment.

More from this blog