Skip to main content

Command Palette

Search for a command to run...

Map & Set

Leveraging Key-Value Pairs and Unique Collections in ES6+

Published
6 min read

Two situations come up all the time in JavaScript. You need to store key-value pairs where the keys are not strings. Or you need a list where every item appears exactly once. Both times, most people reach for a plain object or an array. Both times, the tool kind of works but keeps surprising you in annoying ways.

User1 is building a permission system. They use a plain object to map user IDs to roles. Then they discover that obj.toString already exists as a key, so their code silently returns a built-in function instead of undefined. User2 is tracking which pages a visitor has seen. They push into an array and later realize the same URL got added three times. Both of them needed a different tool.

Objects and arrays are not wrong. They're just not built for these jobs. Map and Set are.

What Map is

A Map stores key-value pairs, exactly like an object. The difference is what it allows as keys. Objects only accept strings and symbols. A Map accepts anything: strings, numbers, booleans, other objects, DOM nodes, even functions.

It also keeps insertion order. You iterate a Map and the entries come back in the order you put them in. Objects are not guaranteed to do that, especially when the keys are numeric-looking strings.

const scores = new Map();

// String key 
scores.set("User1", 42);

// Number key- objects would convert this to a string, Map won't
scores.set(1001, 88);

// Object as key 
const userObj = { id: 7 };
scores.set(userObj, 95);

console.log(scores.get("User1"));   // 42
console.log(scores.get(1001));     // 88
console.log(scores.get(userObj));  // 95
console.log(scores.size);          // 3

Reading a key that doesn't exist returns undefined. Not a built-in function from the prototype chain. Not an inherited property. Just undefined. That alone removes a whole class of subtle bugs that plain objects invite.

What Set is

A Set is a list where every value is unique. You add things to it and it keeps track of what it has already seen. Try adding the same value twice and the second one just gets ignored, no error, no duplicate.

That's it. The whole point is uniqueness. If you need a collection where order matters and duplicates are fine, use an array. If you need to track whether something exists and make sure there is only one of each, use a Set.

const visited = new Set();

visited.add("/home");
visited.add("/about");
visited.add("/home");  // duplicate gets silently ignored
visited.add("/contact");

console.log(visited.size);              // 3, not 4
console.log(visited.has("/about"));    // true
console.log(visited.has("/cart"));     // false

// Works for primitives and objects both
const ids = new Set([1, 2, 3, 2, 1]);
console.log([...ids]);  // [1, 2, 3]

One of the most common uses for Set is deduplication. Spread an array into a Set, then spread it back. Two characters of code, duplicates gone. No sorting, no filter with indexOf, no extra library.

// deduplicate an array
const tags = ["js", "css", "js", "html", "css"];
const unique = [...new Set(tags)];
console.log(unique); // ["js", "css", "html"]

Map vs Object

The instinct to reach for a plain object is strong. Objects are older, they're everywhere, and they look simpler. But they come with a few gotchas that Map avoids by design.

The biggest one is the prototype. Every plain object inherits properties from Object.prototype. That means keys like "constructor", "toString", or "hasOwnProperty" already exist on your object before you add anything. A Map starts completely empty.

// Plain object. prototype keys already exist
const obj = {};
console.log(obj.toString); // [Function: toString] = not undefined!

// Map, empty
const map = new Map();
console.log(map.get("toString")); // undefined = clean
// non-string keys
// Object silently converts numeric keys to strings
const obj = {};
obj[1]   = "one";
obj["1"] = "overwritten"; // same key = both point to "overwritten"

// Map keeps types intact
const map = new Map();
map.set(1,   "one");
map.set("1", "string one"); // different keys, both live
console.log(map.size); // 2
Map Object
Any key type (string, number, object, function) String and Symbol keys only
No inherited prototype keys Inherits from Object.prototype
.size is a direct property Size needs Object.keys(obj).length
Insertion order is preserved Numeric key order is implementation-defined
Built-in iteration with for...of JSON serialization works natively
Better performance for frequent add/delete Better for static configs and known shapes

Set vs Array

Arrays are ordered lists that allow duplicates and give you a ton of methods: map, filter, reduce, index access. Set is a membership structure. It tells you whether something is in the collection, quickly, and it enforces uniqueness automatically.

The tradeoff is simple. You cannot access a Set element by index. There is no set[0]. If you need that, use an array. But if you are asking "does this thing exist?" many times, Set is faster. Arrays run through every element with includes(). Set uses a hash lookup and answers in constant time.

const arr = ["User1", "User2", "User3"]; // could be thousands
const set = new Set(["User1", "User2", "User3"]);

// Array: scans from start, O(n)
arr.includes("User3"); // walks the whole list to be sure

// Set: hash lookup, O(1)
set.has("User3"); // answers immediately regardless of size
Set Array
Unique values, no duplicates possible Allows duplicates
.has() is fast regardless of size .includes() is O(n)
No index access Index access with arr[i]
No map or filter built in Method map, filter, reduce, sort available

When to Use Each One

Most of the confusion around Map and Set comes from not having a clear rule for when to switch. Here is a fairly clean one: if the answer to your problem involves the word "unique" or "exists", reach for Set. If it involves "look up by key" and the key is not a string, or you need guaranteed ordering, reach for Map.

// map
const user1 = { name: "User1", id: 1 };
const user2 = { name: "User2", id: 2 };

const scores = new Map();
scores.set(user1, 980);
scores.set(user2, 740);

// Iterate in insertion order
for (const [user, score] of scores) {
  console.log(`\({user.name}: \){score}`);
}
// User1: 980
// User2: 740
// set
// Track which users completed onboarding (no duplicates)
const completed = new Set();
completed.add("User1");
completed.add("User2");
completed.add("User1"); // ignored

function canProceed(userId) {
  return completed.has(userId);
}
console.log(canProceed("User1")); // true
console.log(canProceed("User3")); // false

// Remove an entry
completed.delete("User2");
console.log(completed.size); // 1

A useful mental shortcut: if you are storing a pair of things together, Map. If you are storing a single thing and just need to know it's there, Set.

What You Actually Get Out of This

REFERENNCES:

Simply JavaScript

Part 5 of 25

JavaScript is a quirky language. To master it, one should know to avoid its hidden traps along with its logic. This series showcase my journey through JS: the pain points, the breakthroughs, and the coding standards that I adopted from my mentors.

Up next

Async JavaScript

Why your code doesn't always run top to bottom