Map and Set in JavaScript
JavaScript has always had objects for storing key-value pairs and arrays for storing ordered collections of values. For most situations, they work well. But both come with limitations that become noticeable in specific scenarios, and those limitations led to two additions in ES6: Map and Set.
Map and Set are not replacements for objects and arrays. They are purpose-built alternatives that handle certain tasks more cleanly. Understanding what each one is, where it differs from the familiar options, and when to reach for it makes your code more precise and sometimes significantly more efficient.
What Map Is
A Map is a collection of key-value pairs, just like an object. The difference is in how it behaves and what it allows.
You create a Map and work with it through dedicated methods:
const userRoles = new Map();
userRoles.set("alice", "admin");
userRoles.set("bob", "editor");
userRoles.set("charlie", "viewer");
userRoles.get("alice"); // admin
userRoles.has("bob"); // true
userRoles.size; // 3
userRoles.delete("charlie");
.set() adds or updates an entry. .get() retrieves a value by key. .has() checks whether a key exists. .size returns the number of entries directly as a property. .delete() removes a specific entry.
Iterating over a Map respects insertion order:
for (const [key, value] of userRoles) {
console.log(key, value);
}
// alice admin
// bob editor
The destructured [key, value] pattern comes naturally from how Map entries are structured.
What Set Is
A Set is a collection of unique values. Every value in a Set appears exactly once. If you try to add a value that already exists, nothing happens. The Set stays unchanged.
const tags = new Set();
tags.add("javascript");
tags.add("nodejs");
tags.add("javascript"); // duplicate, ignored
tags.size; // 2
tags.has("nodejs"); // true
tags.delete("nodejs");
Like Map, Set uses methods for all its operations. .add() inserts a value. .has() checks for existence. .delete() removes a value. .size gives the count.
The most immediate use of a Set is deduplication. Converting an array to a Set removes all duplicates instantly:
const numbers = [1, 2, 2, 3, 3, 3, 4];
const unique = [...new Set(numbers)];
// [1, 2, 3, 4]
The spread operator converts the Set back into an array. Two lines and the job is done.
Map vs Object
Objects are the default choice for key-value storage in JavaScript, and they work well for most purposes. But they carry some characteristics that become problems in specific situations.
Key types. Object keys must be strings or symbols. If you use anything else as a key, JavaScript silently converts it to a string. A number key 42 becomes the string "42". Map keys can be anything at all: numbers, objects, arrays, functions, even other Maps.
const cache = new Map();
const keyObject = { id: 1 };
cache.set(keyObject, "cached result");
cache.get(keyObject); // cached result
Using an object as a key is something that simply cannot be done with a plain object.
Inherited keys. Every plain object inherits properties from Object.prototype. This means checking whether a key exists with "toString" in obj returns true even though you never set it. Map has no inherited keys. Every entry in a Map was explicitly added.
Size. Getting the number of entries in an object requires Object.keys(obj).length, which creates a temporary array just to count it. Map gives you map.size directly.
Iteration order. Plain objects technically preserve insertion order for string keys in modern JavaScript, but it was not always guaranteed and the spec has some edge cases around numeric keys. Map always preserves insertion order with no exceptions.
Performance under frequent changes. When you are constantly adding and removing entries, Map is generally faster than an object. The Map data structure is optimized for this pattern in a way that plain objects are not.
The practical comparison looks like this:
| Object | Map | |
|---|---|---|
| Key types | Strings and symbols only | Any type |
| Inherited keys | Yes | No |
| Getting size | Object.keys(obj).length |
map.size |
| Insertion order | Mostly guaranteed | Always guaranteed |
| Best for | Structured records with known keys | Dynamic key-value storage |
Set vs Array
Arrays are ordered collections that allow duplicate values. Sets are unordered collections that do not.
Uniqueness. The defining difference. Arrays hold whatever you put in them, duplicates included. Sets enforce uniqueness automatically. You never need to check before adding.
Membership testing. Checking whether a value exists in an array requires .includes(), which scans through every element until it finds a match. For large arrays, this is slow. Checking whether a value exists in a Set with .has() is much faster because Sets use a hash-based structure internally. The lookup time does not grow with the size of the Set.
// Array: scans every element
const arr = [1, 2, 3, 4, 5];
arr.includes(4); // true, but checks each element
// Set: near-instant regardless of size
const set = new Set([1, 2, 3, 4, 5]);
set.has(4); // true, fast
No index access. Arrays give you elements by position: arr[2]. Sets do not have positional access. You can iterate over a Set or check membership, but you cannot ask for the third item. If you need positional access, you need an array.
Set operations. Sets make it straightforward to compute unions, intersections, and differences between collections:
const a = new Set([1, 2, 3, 4]);
const b = new Set([3, 4, 5, 6]);
const union = new Set([...a, ...b]);
// {1, 2, 3, 4, 5, 6}
const intersection = new Set([...a].filter(x => b.has(x)));
// {3, 4}
const difference = new Set([...a].filter(x => !b.has(x)));
// {1, 2}
These operations are possible with arrays but far more cumbersome and less efficient.
| Array | Set | |
|---|---|---|
| Duplicates | Allowed | Not allowed |
| Membership check | .includes(), linear scan |
.has(), fast lookup |
| Positional access | Yes | No |
| Order | Preserved | Preserved (insertion order) |
| Best for | Ordered sequences, indexed access | Unique values, membership testing |
When to Use Map and Set
Reach for Map when:
The keys are not strings. If you are using numbers, objects, or any other type as keys, Map is your only real option without manual conversion.
You need the size frequently. If counting entries is part of your logic, map.size beats Object.keys(obj).length for both readability and performance.
You are building a cache, a registry, or a lookup table where entries are added and removed dynamically. Map is optimized for this access pattern.
You need guaranteed iteration order and want to be explicit about it.
Reach for Set when:
You need a collection of unique values and want the uniqueness enforced automatically rather than managed manually.
You need fast membership testing on a large collection. The performance difference between .has() on a Set and .includes() on an array becomes meaningful at scale.
You are computing relationships between collections: what do these two groups have in common, what is in one but not the other.
You are deduplicating data from any source.
Wrapping Up
Objects and arrays remain the right choice for the majority of situations. When you have a structured record with known string keys, an object is fine. When you have an ordered list that may contain duplicates and needs index access, an array is right.
Map and Set fill the gaps. Map handles key-value storage when keys are not strings, when size matters, or when you are building something that changes frequently. Set handles collections where uniqueness and fast membership testing are the priorities.
Knowing both options and choosing deliberately, rather than defaulting to objects and arrays out of habit, is what makes the difference between code that works and code that is genuinely well-suited to the problem it is solving.


