Set and Map usage patterns
Set and Map are built on the same hash mechanism but answer different questions:
- Set — the "does this value exist?" question. It stores membership only, with no value. Its two main jobs: removing duplicates (dedup) and keeping an "already seen" record.
- Map — the "what corresponds to this key?" question. It builds a key → value index.
Simple rule: if the answer is yes/no, use a Set; if the answer is some data, use a Map.
The patterns you will meet most often:
- Dedup:
[...new Set(emails)] - Membership check:
if (blocked.has(userId))—includeson an array is O(n), a Set is O(1) - Building an index:
const byId = new Map(users.map(u => [u.id, u]))— every later lookup is O(1) - Grouping: key → the list of elements belonging to that key
- Counting: key → how many times it appeared
Key equality — this is where the languages genuinely diverge, and it is the most common interview trap.
JavaScript: Map/Set use SameValueZero comparison. For primitives that behaves like === (except NaN, which counts as equal to itself). Object keys, however, compare by reference — call m.set({ id: 1 }, 'a') and then m.get({ id: 1 }) and you get undefined, because that is a second, entirely different object.
If you need value-based keys in JS, you have to build the key as a string yourself (` ${a}:${b} `).
Kotlin: equality is defined by the equals()/hashCode() pair. A data class generates both automatically, so value-based keys come for free: Point(1, 2) == Point(1, 2) → true, therefore the same key in a map. A plain class compares by reference — the same trap as JS.
Dart: the default is reference-based. For value semantics you must override operator == and hashCode together — writing one and forgetting the other breaks the map. To avoid the boilerplate people use package:equatable or freezed; in Dart 3 records ((1, 2)) already compare by value and can be used as ready-made keys.
| Question | JavaScript | Kotlin | Dart |
|---|---|---|---|
| How do you get a value-based key? | By building a string key by hand | `data class` — automatic | Override `==` + `hashCode`, or a Dart 3 record |
| Iteration order | `Map`/`Set` preserve insertion order (guaranteed) | `HashMap` — none; `LinkedHashMap` — insertion; `TreeMap` — sorted | The `{}` literal is a `LinkedHashMap` — insertion order; `HashMap` — none |
| If you need keys in sorted order | No built-in — extract the keys and `sort` | `sortedMapOf` / `java.util.TreeMap` | `SplayTreeMap` (`dart:collection`) |
| Size | `map.size` | `map.size` | `map.length` |
When is a plain object/record not a Map?
In JS many people use {} as a map. For a small, fixed configuration that is fine, but for dynamic keys Map is almost always the right choice:
- Object keys can only be
stringorsymbol—obj[1]is reallyobj["1"].Mapaccepts any type as a key, even an object. - An object inherits the prototype chain:
"toString" in objistrueeven if you added nothing. AMapstarts clean. - Keys like
constructoror__proto__produce surprises (prototype pollution risk). - Size:
Object.keys(obj).lengthis O(n),map.sizeis O(1). - Frequent insert/delete:
Mapis optimised for it, plain objects are not.
Rule: object/record when the keys are known and fixed up front, Map when the keys arrive at runtime. Kotlin and Dart have no such confusion, because there an object (data class) and a map are different things at the language level.
Interview tip. The most frequent question here: "Can you use an object as a Map key?" The weak answer is just "yes". The strong answer separates the languages:
"Yes, but what matters is how equality is defined. In JS object keys compare by reference, so two separate objects with identical contents are different keys — if I need value-based keys I build a string key myself. In Kotlin a data class generates equals/hashCode, so it works by value. In Dart I have to override == and hashCode together."
A second classic: "What is the difference between Object.keys().length and map.size?" — the answer is O(n) versus O(1). Interviewers use this small detail to probe your feel for performance.
The most commonly missed point: giving a wrong guarantee about iteration order. JS Map order IS guaranteed; Kotlin HashMap order is NOT. Confusing the two is a real source of bugs.
📚 Sources and documentation
- Setofficialdeveloper.mozilla.org