# Map and Set

Source: https://codewiki.com/javascript/map-set/

> - **what**: `Map` stores values by key, while `Set` stores unique members. Both are directly iterable and preserve insertion order.
> - **trap**: Object keys and members compare by identity, not by fields. A `get()` result of `undefined` also doesn't prove that a key is absent.
> - **fix**: Define a canonical key representation, use `has()` to distinguish absence, and use Node 24's native `Set` methods for set algebra.

## What it is and why it exists

A JavaScript `Map` is an ordered map that associates each unique key with a value. Keys and values can be any JavaScript value; keys don't first need to become property names. A `Set` holds unique values and represents membership rather than elements at particular positions.

Plain objects also associate names with values, but their own property keys can only be strings or Symbols. Other values, including numbers, are converted to property keys, and an object can't remain an object when used as a property key. Objects fit records with known fields; `Map` fits keys discovered at runtime, object keys, and key-value collections that you need to iterate directly.

Arrays retain order and duplicates, but they don't enforce uniqueness. Repeatedly calling `includes()` for membership treats a sequence as a set and obscures the intent. `Set` puts uniqueness and `has()` membership in the data structure's contract while retaining insertion-order iteration.

`WeakMap` and `WeakSet` are weak collections. They let metadata or markers follow the reachable lifetime of object keys without exposing enumeration or a size. A weak collection is not a lighter general-purpose `Map` or `Set`, because you can't list the entries that are currently alive.

These four collections solve different modeling problems. First ask whether the data is a record, sequence, mapping, or membership set, then ask whether the collection should keep its keys alive. Similar method names don't make the structures interchangeable.

### Choose the collection shape

| Requirement | Prefer | Reason |
|---|---|---|
| Business record with fixed fields | `Object` | Natural property access, destructuring, and JSON shape |
| Runtime keys mapped to values | `Map` | Arbitrary key types and direct entry iteration |
| Ordered sequence with duplicates | `Array` | Indexing and sequence transformations |
| Unique members and set relations | `Set` | Uniqueness, membership, and set operations |
| Metadata that follows an object key's reachability | `WeakMap` | The collection doesn't keep the key alive |
| Marker that follows an object's reachability | `WeakSet` | Records only whether an object is marked |

This table describes semantics, not a performance ranking. Actual speed depends on the engine, key distribution, collection size, and operation mix. Choose the structure that expresses the invariant, then measure a hot path.

## How it works

### Four interfaces

The core `Map` operations are `set(key, value)`, `get(key)`, `has(key)`, and `delete(key)`. Its `size` reports the entry count, and `clear()` removes every entry. `set()` returns the receiver, so calls can be chained. `delete()` reports whether an existing key was actually removed.

The corresponding core `Set` operations are `add(value)`, `has(value)`, and `delete(value)`. `add()` also returns the receiver, and adding an existing value doesn't increase `size`. `clear()` removes every member.

| Collection | Contents | Enumerable | Size | Write operation |
|---|---|---:|---:|---|
| `Map` | Keys and values | Yes | `size` | `set()` |
| `Set` | Unique values | Yes | `size` | `add()` |
| `WeakMap` | Weak keys and values | No | None | `set()` |
| `WeakSet` | Weak values | No | None | `add()` |

The default `Map` iterator and `entries()` both produce `[key, value]` pairs. `keys()` and `values()` produce only their respective side. The default `Set` iterator and `values()` produce members; for consistency with `Map`, its `keys()` also produces members and `entries()` produces `[value, value]` pairs.

### Construction and copying

`new Map(iterable)` builds a mapping from iterable key-value pairs, such as a two-dimensional array, another `Map`, or `Object.entries(record)`. `new Set(iterable)` reads each value from an iterable, so arrays, strings, and another `Set` all work as input. Both constructors create entries in read order.

`new Map(existingMap)` and `new Set(existingSet)` create new collection containers, but they don't deep-copy contained objects. If keys, members, or values are objects, the old and new collections still reference the same objects. A field mutation on one of those objects is observable through both collections.

`Object.fromEntries(map)` fits a mapping whose string or Symbol keys should become a record. Other keys are converted to property keys, so object keys can all collapse into one `"[object Object]"` property. Confirm that this information loss matches the interface contract before converting.

### Equality rules

`Map` keys and `Set` members both use SameValueZero equality. It mostly behaves like `===`, except that all `NaN` values are equal and `+0` equals `-0`. A collection can therefore contain only one `NaN` and one zero value.

Objects, arrays, and functions compare by object identity. Two objects with equal fields are different keys or members unless they are the same object. Collections don't invoke a custom hash function or perform automatic deep content comparison.

| Left | Right | Same in `Map` or `Set` |
|---|---|---:|
| `NaN` | `NaN` | Yes |
| `+0` | `-0` | Yes |
| `7` | `'7'` | No |
| One object reference | The same object reference | Yes |
| `{ id: 7 }` | `{ id: 7 }` | No |

If the business rule deduplicates by `id`, use the stable `id` as the key or first produce an explicit canonical key. Storing a temporary object as a key and reconstructing an equal-looking object for lookup won't find the entry. Using `JSON.stringify()` as an improvised composite key also requires rules for property order, missing values, and unsupported types.

### Insertion order and iteration

`Map` and `Set` iterate in the order of first successful insertion. Updating an existing `Map` value doesn't move its key, and adding an existing `Set` member doesn't move it. Deleting and inserting again creates a new order position at the current end.

That order is collection semantics, not business sorting. If display output must follow time, priority, or localized names, convert entries to an array and write the comparison rule. Depending on accidental arrival order makes output change with an upstream query or network batch.

Iterators read a live collection, not a snapshot captured at creation. An entry deleted before it is visited won't appear; one added before iteration ends may appear. Deleting an already visited entry and inserting it again can cause it to be visited at its new position.

Updating the current key's value during traversal is usually understandable; reordering keys during traversal is much harder to review. If a loop must delete and reinsert, iterate a `[...map]` or `[...set]` snapshot, or record changes in a separate collection and apply them after the loop.

### Absence and `undefined`

`map.get(key)` returns `undefined` when a key is absent, but a present key may also explicitly store `undefined`. The return value alone can't distinguish these states. Call `map.has(key)` when that distinction matters, then read the value.

`map.get(key) || fallback` also treats valid values such as `0`, `false`, and the empty string as absent. Use `??` only when both `null` and `undefined` mean absence; when “not stored” differs from “stored as `undefined`,” you still need `has()`.

Counting code often uses `counts.set(key, (counts.get(key) ?? 0) + 1)`. The `??` is appropriate because the counter contract doesn't treat `undefined` as a stored count. If the value domain genuinely allows `undefined`, spell out the missing-key branch.

### Native set operations

Node 24's `Set` provides `union()`, `intersection()`, `difference()`, and `symmetricDifference()`. They return new sets without mutating either input. The relation methods `isSubsetOf()`, `isSupersetOf()`, and `isDisjointFrom()` return Booleans. Their names expose set algebra more clearly than scattered combinations of spread, `filter()`, and `every()`.

| Expression | Meaning | Result type |
|---|---|---|
| `a.union(b)` | Member of at least one side | `Set` |
| `a.intersection(b)` | Member of both sides | `Set` |
| `a.difference(b)` | Member of `a` but not `b` | `Set` |
| `a.symmetricDifference(b)` | Member of exactly one side | `Set` |
| `a.isSubsetOf(b)` | Whether every `a` member is in `b` | `boolean` |
| `a.isSupersetOf(b)` | Whether every `b` member is in `a` | `boolean` |
| `a.isDisjointFrom(b)` | Whether the sides share no member | `boolean` |

The receiver must be an actual `Set`; the argument only needs the set-like protocol: a numeric `size`, a `has()` method, and a `keys()` method that returns a member iterator. `Map` satisfies that protocol and behaves as a set of its keys. Arrays don't qualify because they lack `size` and `has()`, and their `keys()` produces indices.

### Weak-collection boundaries

In Node 24, `WeakMap` keys and `WeakSet` members must be garbage-collectable values: objects or non-registered Symbols. A registered symbol from `Symbol.for()` isn't allowed because the global registry makes it retrievable again. Strings, numbers, Booleans, `null`, and `undefined` can't be weak keys either.

A key isn't kept reachable merely because it is in a weak collection. After the object is no longer reachable elsewhere in the program, the engine may collect it and make the associated entry disappear. The engine decides when collection happens; an application can't observe or force that moment.

Because collection time is uncertain, weak collections have no `size`, `clear()`, or iterator. If keys were enumerable, code could observe garbage-collection decisions or make keys reachable again while inspecting them. Use an ordinary `Map` with explicit lifetime management when you must list a cache, enforce capacity, or produce metrics.

`WeakMap` fits parsed results, DOM metadata, or external state associated with an object without modifying that object. `WeakSet` fits the question “has this object been processed?” Neither replaces explicit unsubscription, transaction closing, or sensitive-data cleanup.

## Examples

### Key identity and unique members

The first example puts equal-looking order objects, a numeric key, and a string key in a `Map`, then observes SameValueZero deduplication in a `Set`. Its output depends only on language rules, not on an object's debug display format.

<!-- quick -->

```javascript
// file: key_identity.js
const firstOrder = { id: 'A-17' };
const sameFields = { id: 'A-17' };

const statusByOrder = new Map();
statusByOrder.set(firstOrder, 'queued');
statusByOrder.set(7, 'numeric key');
statusByOrder.set('7', 'string key');

console.log(statusByOrder.get(firstOrder));
console.log(statusByOrder.get(sameFields));
console.log(statusByOrder.get(7));
console.log(statusByOrder.get('7'));

const observed = new Set([NaN, NaN, 0, -0, '0']);
console.log(observed.size);
console.log([...new Set(['draft', 'sent', 'draft'])].join(','));
```

```text
queued
undefined
numeric key
string key
3
draft,sent
```

<!-- /quick -->

`firstOrder` finds the entry because the lookup uses the same object. `sameFields` only has equal fields, so the result is `undefined`. The final set contains three distinct members: `NaN`, zero, and the string `'0'`.

### Build an index and a member set together

This example uses a `Map` for a primary-key order index and a `Set` for deduplicated reviewer IDs. A repeated order is an input-contract violation, so the function throws before overwriting an older entry.

```javascript
// file: index_orders.js
function indexOrders(orders) {
  const byId = new Map();
  const reviewerIds = new Set();

  for (const order of orders) {
    if (byId.has(order.id)) {
      throw new Error(`duplicate order: ${order.id}`);
    }
    byId.set(order.id, order);
    for (const reviewerId of order.reviewerIds) {
      reviewerIds.add(reviewerId);
    }
  }

  return { byId, reviewerIds };
}

const orders = [
  { id: 'A-17', total: 18, reviewerIds: ['u1', 'u2'] },
  { id: 'A-18', total: 24.5, reviewerIds: ['u2', 'u3'] },
];

const index = indexOrders(orders);
console.log(index.byId.get('A-18').total);
console.log([...index.reviewerIds].join(','));
console.log([...index.byId.keys()].join(' -> '));
```

```text
24.5
u1,u2,u3
A-17 -> A-18
```

`u2` appears on two orders, but the `Set` retains it once. Both collections keep first-insertion order, so the output is predictable. A business requirement to sort reviewers by name would still need a separate sorting step.

### Compose eligibility sets

Set operations can express an eligibility rule directly as membership. The ready list contains people who are eligible and trained but not blocked, without mutating any of the three inputs.

```javascript
// file: set_composition.js
const eligible = new Set(['ana', 'bo', 'chen']);
const trained = new Set(['bo', 'chen', 'dara']);
const blocked = new Set(['chen']);

const ready = eligible
  .intersection(trained)
  .difference(blocked);

console.log([...ready].join(','));
console.log([...eligible.union(trained)].join(','));
console.log([...eligible.symmetricDifference(trained)].join(','));
console.log(ready.isSubsetOf(eligible));
console.log(ready.isDisjointFrom(blocked));
```

```text
bo
ana,bo,chen,dara
ana,dara
true
true
```

`ready` contains only `bo`; it is a subset of `eligible` and disjoint from `blocked`. `union()` and `symmetricDifference()` return new sets, so the original rule sets remain available for later work.

### Tie metadata to object lifetime

This example records per-schema validation counts in a `WeakMap` and prevents repeat visits to one order object with a `WeakSet`. A local, non-registered Symbol is also a valid weak key in Node 24.

```javascript
// file: weak_metadata.js
const validationRuns = new WeakMap();
const visited = new WeakSet();

function validate(schema, input) {
  const previous = validationRuns.get(schema) ?? 0;
  validationRuns.set(schema, previous + 1);
  const valid = schema.required.every((key) => Object.hasOwn(input, key));
  return `${schema.name}:${valid}:${validationRuns.get(schema)}`;
}

function visitOnce(record) {
  if (visited.has(record)) return false;
  visited.add(record);
  return true;
}

const orderSchema = { name: 'order', required: ['id', 'total'] };
const order = { id: 'A-17', total: 18 };

console.log(validate(orderSchema, order));
console.log(validate(orderSchema, { id: 'A-18' }));
console.log(visitOnce(order), visitOnce(order));

const requestMarker = Symbol('request');
validationRuns.set(requestMarker, 1);
console.log(validationRuns.has(requestMarker));
```

```text
order:true:1
order:false:2
true false
true
```

The count belongs to the identity of `orderSchema`, not to the content of its fields. The example doesn't try to prove that garbage collection occurred; ordinary program output can't reliably verify that timing.

## Pitfalls

### Looking up with a new equal-looking object

> **Pitfall:** Generated code often calls `map.set({ id }, value)` and later looks up `map.get({ id })`. The two objects have equal fields but different identities, so the lookup never finds the entry.

**Fix:** if `id` defines business identity, use `id` itself as the key. If object keys are required, retain and pass the canonical object reference, and document the identity semantics in the interface.

### Treating falsy values as absent

> **Pitfall:** `map.get(key) || defaultValue` overwrites valid `0`, `false`, and empty-string values. Testing only whether `get()` returns `undefined` also conflates an absent key with a stored `undefined`.

**Fix:** choose the check from the value domain. Use `??` when only nullish values mean absence; use `has()` when key presence itself matters, and test absence, `undefined`, zero, and `false` separately.

### Expecting `Set` to deduplicate object content

> **Pitfall:** `new Set([{ id: 1 }, { id: 1 }])` has size `2`. `Set` deduplicates by object identity and doesn't recursively compare fields.

**Fix:** to deduplicate by a stable business key, use a `Map` from that key to the object you want to retain, and decide explicitly whether first or last wins. Don't treat generic `JSON.stringify()` as deep equality without a contract.

### Deleting and reinserting during iteration

> **Pitfall:** Code that deletes the current entry and reinserts it to “refresh” order may visit the entry again at the end of live iteration. Repeating that operation can even prevent the loop from ending.

**Fix:** iterate a snapshot, or collect reorder intentions and apply them after the loop. If you only need to update an existing `Map` value, call `set()` directly without deleting the key first.

### Serializing collections directly to JSON

> **Pitfall:** `JSON.stringify(new Map([['a', 1]]))` and `JSON.stringify(new Set(['a']))` both produce `'{}'` by default. JSON serialization reads enumerable own string properties, not collection entries.

**Fix:** define a wire format first. A string-keyed record can use `Object.fromEntries()`; when key types and order matter, encode a `Map` as an entry array and a `Set` as a value array, validate on input, then rebuild the collection.

### Treating a weak collection as an observable cache

> **Pitfall:** Generated cache code may read `weakMap.size`, iterate its keys, or assert that an entry has been collected at a fixed time. Weak collections deliberately omit those operations, and garbage-collection timing isn't a business event.

**Fix:** use an ordinary `Map` with an explicit policy when you need capacity, eviction, metrics, or enumeration. Use `WeakMap` only to query attached data through a weak key you still hold; resources still need an explicit `close()`, unsubscribe function, or `finally`.

<!-- deep -->

## Equality, iteration, and weak reachability

### The specified complexity guarantee

ECMAScript requires `Map` and `Set` implementations to provide access times that are sublinear on average, but it neither requires a hash table nor promises `O(1)` for every operation. An engine may use a hash table, tree, or another structure that meets the observable semantics and complexity requirement. Application code can't depend on bucket counts, hash values, or resize timing.

That guarantee rules out implementing every membership lookup as a full collection scan, but it doesn't replace a benchmark. Converting an array to a `Set` has a construction cost, so one lookup may not justify the conversion. The model becomes natural when the same member set serves repeated queries. Any performance conclusion must name the target engine, data size, key type, and read-write mix.

### Order state transitions

Insertion order belongs to entry state, not to a permanent timestamp on the key. The following transitions apply to `Map` keys; `Set` members follow the same rules.

| Operation | Changes size | Changes position |
|---|---:|---:|
| Insert a new key | Yes | Appends at the end |
| Update an existing key | No | No |
| Add an existing member | No | No |
| Delete an existing key | Yes | Removes the position |
| Insert after deletion | Yes | Appends at the end |
| Insert after `clear()` | Yes | Starts a new order |

Live traversal makes collection changes affect an unfinished iterator immediately. If you update an unvisited key's value, the iterator observes the new value; if you delete the key first, it doesn't observe the key. New entries added before traversal ends may be visited.

This behavior can implement a work queue, but ordinary business loops usually need stable input. If new entries belong to the next pass, copy keys or entries at the start. When the data is too large to copy, maintain an explicit batch boundary instead of letting live iterator semantics define the scheduling policy implicitly.

### Set-like arguments and result order

Set composition methods consume the right-hand argument through `size`, `has()`, and `keys()`, not through its default iterator. That design lets a `Map` participate as a set of keys and explains why an array doesn't qualify. The three members on a custom set-like object must agree with each other or the result has no reliable meaning.

`intersection()` may choose the smaller side for iteration based on their sizes, so don't treat its result order as a filtered copy of the left set. For example, if the left side is `a, b, c` and the smaller right side is `c, a`, Node 24 returns the intersection in `c, a` order. Sort explicitly after the set operation when output needs domain ordering.

Relation methods can return early from a size comparison or once they find enough evidence. Don't put side effects in a set-like object's `has()` or `keys()` methods. The specification's allowed call order is part of the protocol contract, not a business event stream.

### Why weak keys aren't enumerable

A weak key expresses conditional reachability: the collection itself doesn't keep the key alive, but while the program can still obtain the key, it can use it to retrieve the associated value. A `WeakMap` value can be any value and remains retrievable through a reachable key. Whether and when collection occurs belongs to the engine's garbage collector.

If a weak collection exposed a key list or size, the same code could produce different results after an unpredictable garbage collection. Worse, enumeration would reacquire objects that were about to become unreachable. Removing those observation APIs lets an engine collect keys without changing visible program logic.

A non-registered Symbol can't be recovered from the global registry, so the current specification permits it as a weak key. The result of `Symbol.for(name)` is retained by the global symbol registry and isn't a garbage-collectable key; passing it to `WeakMap.set()` or `WeakSet.add()` throws `TypeError`.

Weak collections don't create a security boundary. Code holding both the `WeakMap` and its key can still read the value, and code holding the object may expose data through another path. Prefer private fields for state owned by one class; `WeakMap` fits metadata owned outside the object's implementation or shared across object types.

Finally, weak reachability isn't resource management. File handles, subscriptions, locks, and transactions need release in deterministic control flow. Garbage collection only manages memory reachability and can't promise that an external system observes cleanup before a deadline.

<!-- /deep -->

[Checkpoint: javascript/map-set](https://codewiki.com/javascript/map-set/#checkpoint)

## Further reading

- [ECMAScript language specification: keyed collections](https://tc39.es/ecma262/multipage/keyed-collections.html)
- [MDN: `Map`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Map)
- [MDN: `Set`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Set)
- [MDN: `WeakMap`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/WeakMap)
- [MDN: `WeakSet`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/WeakSet)
