JavaScript Map and Set

Map and Set are built-in JavaScript collection types introduced in ES6. A Map stores key-value pairs where keys can be of any type, and a Set stores a collection of unique values. They solve problems that plain objects and arrays handle awkwardly: objects only allow string or symbol keys and carry inherited properties, while arrays require manual work to keep values unique. Understanding Map and Set well means understanding not just their methods, but how they compare values internally and how they preserve insertion order.

Overview: How Map and Set Work

A Map is an ordered collection of key-value pairs. Unlike a plain object, a Map can use any value as a key — not just strings and symbols. You can use numbers, booleans, functions, objects, or even other Maps as keys. A Set is an ordered collection of unique values, where each value may occur only once, similar to the mathematical concept of a set.

Both structures use an internal algorithm called SameValueZero to decide whether two values are ‘the same’. This is almost identical to strict equality (===), with one important exception: NaN is considered equal to itself under SameValueZero, even though NaN === NaN is false everywhere else in JavaScript. This means a Set can only ever contain one NaN, and a Map can only ever have one entry keyed by NaN.

Internally, engines implement Map and Set using hash tables, similar to how objects store properties, but without the baggage of a prototype chain or the string/symbol key restriction. Because they are hash-table based, get, set, has, and delete all run in average constant time, O(1), regardless of how many entries the collection holds. Both types also remember insertion order: when you iterate a Map or a Set, entries come back in the exact order they were added, which is not guaranteed for all key types in plain objects.

A closely related pair of types, WeakMap and WeakSet, exist for cases where you want object keys that don’t prevent garbage collection. They are covered separately, but it’s worth knowing they exist: a regular Map holds a strong reference to every key, so objects used as keys stay in memory as long as the Map does.

Syntax

const map = new Map(iterable);
const set = new Set(iterable);

The optional iterable argument seeds the collection: for Map it must be an iterable of [key, value] pairs (such as an array of two-element arrays, or another Map); for Set it can be any iterable of values (an array, a string, another Set, and so on).

Map method / property What it does
set(key, value) Adds or updates an entry; returns the map, so calls can be chained
get(key) Returns the value for key, or undefined if it isn’t present
has(key) Returns true if the key exists
delete(key) Removes the entry; returns true if something was removed
clear() Removes every entry
size A property (not a method) holding the number of entries
keys() / values() / entries() Return iterators over keys, values, or [key, value] pairs
Set method / property What it does
add(value) Adds a value; returns the set, so calls can be chained
has(value) Returns true if the value exists
delete(value) Removes the value; returns true if something was removed
clear() Removes every value
size A property holding the number of values

Examples

Example 1: Basic Map usage

const user = new Map();
user.set('name', 'Ava');
user.set('age', 29);
user.set(true, 'admin');

console.log(user.get('name'));
console.log(user.get(true));
console.log(user.size);

for (const [key, value] of user) {
  console.log(`${key} -> ${value}`);
}
Ava
admin
3
name -> Ava
age -> 29
true -> admin

Notice that the key true is a boolean, not the string 'true' — something a plain object could never do, since object keys are always coerced to strings. Iterating with for...of yields [key, value] pairs in the exact order they were inserted.

Example 2: Basic Set usage

const numbers = [1, 2, 2, 3, 3, 3, 4];
const uniqueNumbers = new Set(numbers);

console.log(uniqueNumbers.size);
console.log([...uniqueNumbers]);

uniqueNumbers.add(10);
uniqueNumbers.delete(1);

console.log(uniqueNumbers.has(2));
console.log(uniqueNumbers.has(1));

for (const value of uniqueNumbers) {
  console.log(value);
}
4
[ 1, 2, 3, 4 ]
true
false
2
3
4
10

Passing the array straight into new Set() is the idiomatic way to deduplicate a list. Spreading a set back into an array with [...uniqueNumbers] gives you a plain array again, still in insertion order.

Example 3: A realistic use case

function wordFrequency(text) {
  const counts = new Map();
  const words = text.toLowerCase().match(/[a-z']+/g);

  for (const word of words) {
    counts.set(word, (counts.get(word) || 0) + 1);
  }

  return counts;
}

const text = 'the quick fox jumps over the lazy dog the fox runs';
const freq = wordFrequency(text);

console.log(freq.size);
console.log(freq.get('the'));
console.log(freq.get('fox'));
console.log(freq.get('dog'));

const tags = new Set();
const posts = [
  { title: 'Intro to JS', tags: ['js', 'beginner'] },
  { title: 'Advanced JS', tags: ['js', 'advanced'] },
  { title: 'CSS Grid', tags: ['css', 'beginner'] },
];

for (const post of posts) {
  for (const tag of post.tags) {
    tags.add(tag);
  }
}

console.log(tags.size);
console.log([...tags]);
8
3
2
1
4
[ 'js', 'beginner', 'advanced', 'css' ]

This combines both structures the way real code often does: a Map accumulates counts keyed by word (the counts.get(word) || 0 pattern is a common way to provide a default of zero for a first-time key), while a Set collects the unique tags across every post, automatically discarding duplicates like the repeated 'js' and 'beginner' tags.

Under the Hood: Reference Keys and SameValueZero

const cache = new Map();
const keyObj1 = { id: 1 };
const keyObj2 = { id: 1 };

cache.set(keyObj1, 'first');
cache.set(keyObj2, 'second');

console.log(cache.size);
console.log(cache.get(keyObj1));
console.log(cache.get(keyObj2));
console.log(cache.get({ id: 1 }));

const seen = new Set();
seen.add(NaN);
seen.add(NaN);
console.log(seen.size);
console.log(seen.has(NaN));
2
first
second
undefined
1
true

Even though keyObj1 and keyObj2 look structurally identical, they are two different object references, so the Map treats them as two separate keys — that’s why size is 2. Looking the value up with a brand-new object literal { id: 1 } fails and returns undefined, because that object is yet another reference the Map has never seen. This is a direct consequence of SameValueZero comparing objects by identity, not by shape. The NaN case shows the opposite side of SameValueZero: two additions of NaN collapse into a single entry, because SameValueZero (unlike ===) treats NaN as equal to itself.

Common Mistakes

Mistake 1: Using bracket notation instead of set/get

Because Map instances are objects, JavaScript lets you assign arbitrary properties to them with bracket notation. This compiles fine but silently does the wrong thing — it sets a normal object property, not a Map entry.

const scores = new Map();
scores['alice'] = 90;
scores.set('bob', 85);

console.log(scores.size);
console.log(scores.get('alice'));
console.log(scores['alice']);
1
undefined
90

scores.size is only 1 because 'alice' was never added as a Map entry; it became a plain JavaScript property sitting alongside the Map’s internal data. The fix is to always use set and get:

const scores = new Map();
scores.set('alice', 90);
scores.set('bob', 85);

console.log(scores.size);
console.log(scores.get('alice'));
2
90

Mistake 2: Reading .length instead of .size

Arrays and strings use .length, but Map and Set use .size. Mixing them up is one of the most common slips, and it fails silently because .length simply doesn’t exist on these types.

const items = new Set(['a', 'b', 'c']);

console.log(items.length);
console.log(items.size);
undefined
3

Since Map and Set are not array-like, there is no .length property at all — the expression simply evaluates to undefined rather than throwing an error, which makes the bug easy to miss in larger programs.

Best Practices

  • Reach for a Map when keys aren’t simple strings, when key order matters, or when you need to add and remove keys frequently — plain objects were never designed for that churn.
  • Reach for a Set whenever you need a de-duplicated collection or want fast has() membership checks instead of scanning an array with includes().
  • Convert a Map or Set back to an array with the spread operator ([...map.entries()], [...set]) whenever you need array methods like map, filter, or sort.
  • Use Object.fromEntries(map) to convert a Map into a plain object when you need to serialize it with JSON.stringify, since JSON.stringify does not support Map or Set directly.
  • Prefer new Set(array) over manual loops for deduplication — it is both shorter and clearer about intent.
  • When using objects as Map keys, remember that equality is by reference; if you need value-based key matching, serialize the key (for example with JSON.stringify) or use a string/number derived from its contents instead.
  • Consider WeakMap or WeakSet instead of Map or Set when the keys are objects that should be garbage-collected once nothing else references them, such as metadata attached to DOM nodes.

Practice Exercises

  • Write a function countChars(str) that returns a Map where each key is a character from str and each value is how many times that character appears.
  • Given two arrays, write a function intersection(a, b) that uses two Set instances to return an array of values present in both arrays, with no duplicates.
  • Write a function firstRepeatedValue(array) that uses a Set to find and return the first value in array that appears more than once, or undefined if there is none.

Summary

  • Map stores ordered key-value pairs where keys can be any type; Set stores an ordered collection of unique values.
  • Both use SameValueZero equality: it behaves like === except it treats NaN as equal to itself, so a collection can only hold one NaN.
  • Object keys in a Map are compared by reference, not by structural content.
  • size is a property that reports how many entries or values exist — not .length, and not a method call.
  • Both types preserve insertion order during iteration, unlike some cases with plain object keys.
  • Use set/get/has/delete for Map, and add/has/delete for Set — never bracket notation.
  • WeakMap and WeakSet exist as garbage-collection-friendly alternatives when keys are objects.