JavaScript Sets & Maps

5 minute read

Set and Map are the collection types you reach for after outgrowing plain arrays and objects. A Set stores unique values; a Map stores key-value pairs with any key type and predictable iteration order. Plus there are weak versions (WeakSet, WeakMap) that play nicely with garbage collection. This chapter covers all of them, with the real-world patterns where they outshine the alternatives.

Set — Unique Values

const s = new Set([1, 2, 3, 2, 1]);
console.log(s.size);                  // Output: 3
console.log([...s]);                  // Output: [1, 2, 3]

s.add(4);
s.has(2);                              // true
s.delete(1);
s.clear();

Sets preserve insertion order and use SameValueZero equality (same as ===, but NaN === NaN is true here).

When to Use a Set

Task Set wins because
Deduplicate an array One line: [...new Set(arr)]
Membership test O(1) has vs O(n) Array.includes
Track “seen” items in a loop Built-in, intent-revealing
Set algebra (union, intersection) First-class in ES2025

Dedup an Array

const unique = [...new Set([1, 2, 2, 3, 3, 3])];
// Output: [1, 2, 3]

Set Operations (ES2025)

Modern engines support set algebra directly:

const a = new Set([1, 2, 3]);
const b = new Set([2, 3, 4]);

a.union(b);                            // {1,2,3,4}
a.intersection(b);                     // {2,3}
a.difference(b);                       // {1}
a.symmetricDifference(b);              // {1,4}
a.isSubsetOf(b);                       // false
a.isSupersetOf(new Set([1]));          // true
a.isDisjointFrom(new Set([9]));        // true

If your runtime doesn’t have them yet, the polyfills are tiny.

Iteration

const s = new Set(["a", "b", "c"]);

for (const v of s) console.log(v);
s.forEach(v => console.log(v));
[...s];                                // ["a", "b", "c"]

Map — Key-Value Pairs (Any Key)

const m = new Map();
m.set("name", "Ada");
m.set(42, "answer");
m.set(true, "yes");

const key = { id: 1 };
m.set(key, "object key works");

m.get("name");                         // "Ada"
m.get(key);                            // "object key works"
m.has(42);                             // true
m.size;                                // 4
m.delete("name");

Map vs Plain Object

  Map Object
Key types any (objects, functions, primitives) strings & symbols only
Iteration order insertion order, guaranteed mostly insertion order, but quirky for integer-like keys
Size map.size Object.keys(o).length (O(n))
Defaults / prototype none inherits from Object.prototype (toString, etc.)
Iteration for…of, forEach direct needs Object.keys/entries/values
Serialisation not directly JSON direct JSON
Best for dictionaries with non-string keys, frequent add/remove configs, DTOs, JSON shapes

Tip: If you’re treating a plain object as a hashmap (obj[key] = … in a loop, frequent deletes, unknown keys from input), switch to Map. It avoids prototype pollution, supports any key, and is faster for that pattern.

Initialise from Entries

const m = new Map([
  ["one", 1],
  ["two", 2],
  ["three", 3],
]);

Iteration

for (const [k, v] of m)             console.log(k, v);
for (const k    of m.keys())        console.log(k);
for (const v    of m.values())      console.log(v);
for (const [k, v] of m.entries())   console.log(k, v);
m.forEach((v, k) => console.log(k, v));

The default iterator is entries, so the first form is the idiomatic walk.

Convert To / From Object

const obj = Object.fromEntries(m);     // Map → object  (string/symbol keys only)
const m2  = new Map(Object.entries(obj));   // object → Map

JSON-friendly when the keys are strings.

WeakSet and WeakMap

Same idea, but keys must be objects and are held weakly — the garbage collector can reclaim them even while the set/map exists.

const seen = new WeakSet();

function process(node) {
  if (seen.has(node)) return;
  seen.add(node);
  // …
}

Use cases:

  • Tagging DOM nodes / objects without modifying them — when the node is removed, your tracker stops referencing it automatically
  • Per-instance metadata in a library
  • Caches keyed on object identity that should not leak

Constraints:

  • Keys must be objects (no strings/numbers)
  • Not iterable — no size, no for…of, no keys()
  • You can only add/has/delete (Set) or get/set/has/delete (Map)
const meta = new WeakMap();

class Widget {
  constructor() { meta.set(this, { renders: 0 }); }
  render() { meta.get(this).renders++; }
}

When the Widget is unreferenced, its meta entry vanishes too — no leak.

A Realistic Example — Grouping with a Map

ES2024 added Object.groupBy / Map.groupBy. Before that you’d write it yourself:

const people = [
  { name: "Ada",  team: "core" },
  { name: "Bea",  team: "ui"   },
  { name: "Cleo", team: "core" },
];

const byTeam = new Map();
for (const p of people) {
  if (!byTeam.has(p.team)) byTeam.set(p.team, []);
  byTeam.get(p.team).push(p);
}

byTeam.get("core");
// Output: [{name:"Ada", team:"core"}, {name:"Cleo", team:"core"}]

Modern form:

const byTeam = Map.groupBy(people, (p) => p.team);

Map.groupBy is preferred over Object.groupBy when the grouping key might not be a string.

A Realistic Example — Memoisation with Map

function memoize(fn) {
  const cache = new Map();
  return function (key) {
    if (cache.has(key)) return cache.get(key);
    const v = fn.call(this, key);
    cache.set(key, v);
    return v;
  };
}

const slowSquare = (n) => { /* expensive */ return n * n; };
const fastSquare = memoize(slowSquare);

For object keys with automatic cleanup, swap Map for WeakMap.

A Realistic Example — Set-Based Filter

const admins = new Set(["ada", "bea", "cleo"]);

const adminUsers = users.filter((u) => admins.has(u.name));

Set.has is O(1) — much faster than array.includes when you call it in a loop.

Performance Notes

Operation Object Map Array.includes Set.has
Lookup O(1) avg O(1) avg O(n) O(1) avg
Insertion O(1) O(1) O(1) push O(1)
Delete O(1) (slow path) O(1) O(n) splice O(1)
Memory Lower Higher per entry Lowest Higher per entry

For a few keys, plain object/array often wins on overhead. For anything growing or hot, Map/Set win clearly.

Common Mistakes

// ❌ Trying to use object keys in plain objects
const m = {};
const k = { id: 1 };
m[k] = "x";                            // stringified to "[object Object]"

// ✅ Use Map
const map = new Map();
map.set(k, "x");

// ❌ Forgetting that Set/Map don't JSON-serialise directly
JSON.stringify(new Set([1, 2]));       // "{}"  — loses data

// ✅ Convert first
JSON.stringify([...mySet]);
JSON.stringify(Object.fromEntries(myMap));

// ❌ Comparing with ===
const s = new Set();
s.add({ a: 1 });
s.has({ a: 1 });                       // false — different objects

// ❌ Iterating a WeakMap
for (const x of new WeakMap()) {}      // TypeError — not iterable

// ❌ Using Set assuming deep equality
new Set([[1], [1]]).size;              // 2 — arrays are distinct objects

Best Practices Checklist

  • ✅ Use Set to deduplicate or to test membership in a hot loop
  • ✅ Use Map when keys are objects, integers, or arbitrarily added/removed
  • ✅ Iterate maps directly with for (const [k, v] of map)
  • ✅ Convert to arrays/objects only at serialisation boundaries
  • ✅ Use WeakMap/WeakSet when keying on objects whose lifetimes you don’t control
  • ✅ Prefer Map.groupBy (ES2024) over hand-written grouping
  • ❌ Don’t use a plain object as a hashmap when keys come from user input (prototype pollution risk)
  • ❌ Don’t expect JSON.stringify to handle Set/Map natively
  • ❌ Don’t put primitives in WeakMap/WeakSet — only objects

What’s Next?

You can pick the right container for any data. Next we go one level deeper into how JavaScript iteration works under the hood: iterators and generators — building your own sequences, lazy evaluation, and for await.

← Previous: JS Regular Expressions Next: JS Iterators & Generators →

Updated: