Lesson 34 of 80
Objects make convincing but flawed key-value stores — string-only keys, prototype pollution, no size. Set and Map are the collections actually built for these jobs.
15 min read
Every option explains itself — why the right one is right, and why each wrong one is wrong. Miss one and it comes back later on a spaced-repetition schedule.
This topic is about judgment and practical tradeoffs more than a single right answer — a mock interview is the better way to test yourself on it.
Be interviewed on this →Hints without spoilers, and a plain explanation of why a wrong answer was wrong — answered only from this lesson. Free account, no card.
Sign up freeWhat interviewers actually ask about this, and what a strong answer has to cover.
A strong answer covers
Likely follow-ups
Set stores a collection of unique values — asking "have I seen this before?" and deduplicating are its whole job. Map stores real key-value pairs, where the key can be anything — an object, a function, a number — not just a string.
Both exist because plain objects are a leaky abstraction for these jobs. An object's keys are always coerced to strings (or symbols), it comes with inherited properties from Object.prototype that can collide with real data, it has no built-in .size, and it doesn't guarantee key order the way Map does. Set and Map were added specifically to be the real tool for "unique values" and "arbitrary-key lookup" instead of people reaching for objects and arrays out of habit.
const ids = [3, 1, 4, 1, 5, 9, 2, 6, 5, 3];
const unique = [...new Set(ids)];
// [3, 1, 4, 5, 9, 2, 6] -- order of first appearance, duplicates gone
new Set(ids).size;
// 7 -- Set gives you a real count, no manual counting
Compare that to the object-based version of a lookup table where keys aren't strings:
// The object misuse: this "works" but keys get coerced to strings
const counts = {};
const objA = { name: "a" };
const objB = { name: "b" };
counts[objA] = 1; // key becomes the string "[object Object]"
counts[objB] = 2; // SAME string key -- this overwrites the first entry!
counts[objA]; // 2, not 1 -- objA and objB collided
// The Map version: keys are compared by identity, not stringified
const realCounts = new Map();
realCounts.set(objA, 1);
realCounts.set(objB, 2);
realCounts.get(objA); // 1 -- correct, no collision
realCounts.get(objB); // 2
realCounts.size; // 2
This is the actual bug class Map exists to prevent: any time your "keys" aren't guaranteed to be distinct strings, an object-as-map will silently merge entries that should have stayed separate.
Set uses SameValueZero equality, so NaN is considered equal to itself. new Set([NaN, NaN]).size is 1 — unlike NaN === NaN, which is false. This is the one place Set/Map equality quietly differs from ===.Map everywhere is overkill. The rule of thumb: object for a shape you know ahead of time (a record), Map for a lookup table whose keys are dynamic, unknown at write-time, or non-string.Set/Map are not automatically the faster choice for small collections. For a handful of items, Array.includes() or a plain object often benchmarks just as well or better; Set's O(1) lookup pays off as the collection grows, not by default at every size.Map and Set are the general-purpose versions of a pattern the platform later specialized for memory safety: WeakMap and WeakSet restrict keys to objects and let the garbage collector reclaim entries when nothing else references the key, which is exactly why they exist as a separate, more constrained lesson rather than being folded into this one.