It's duythenights again! Great to have you back.
When should you use Object and when should you use Map? In this post, I'll break it down with a side-by-side comparison, benchmarks, gotchas, and practical rules — cheat sheet style.
Try the Interactive Demo — benchmark Object vs Map with real API data in your own browser.
TL;DR
| Object | Map | |
|---|---|---|
| Key types |
string or Symbol only |
Any value (object, function, number...) |
| Insertion order | Not guaranteed (except integer keys) | Guaranteed |
| Size | Object.keys(obj).length |
map.size — O(1) |
| Iteration |
Object.keys() / for...in
|
for...of / .forEach() — native |
| Prototype pollution | Yes | No |
| Serialization |
JSON.stringify() native |
Manual conversion needed |
| Best for | Struct-like data, fixed shape | Dictionary, dynamic keys, frequent add/delete |
Creating & Basic Operations
Object
// Create
const obj = {};
const obj2 = { name: "Alice", age: 30 };
const obj3 = Object.create(null); // no prototype — safer as dictionary
// Set
obj["key"] = "value";
obj.key = "value";
// Get
const val = obj["key"];
const val2 = obj.key;
// Check
"key" in obj; // true — but checks prototype chain too
obj.hasOwnProperty("key"); // true — own properties only
// Delete
delete obj["key"];
// Size
Object.keys(obj).length;
Map
// Create
const map = new Map();
const map2 = new Map([["name", "Alice"], ["age", 30]]);
// Set
map.set("key", "value");
map.set(42, "number key"); // any type as key
map.set(document.body, "DOM"); // even objects
// Get
const val = map.get("key");
// Check
map.has("key"); // true — no prototype chain issue
// Delete
map.delete("key");
// Size
map.size; // O(1) — direct property
Iteration
Object
const user = { name: "Alice", age: 30, role: "admin" };
// Keys only
Object.keys(user); // ["name", "age", "role"]
// Values only
Object.values(user); // ["Alice", 30, "admin"]
// Entries
Object.entries(user); // [["name","Alice"], ["age",30], ["role","admin"]]
// for...in (includes inherited — usually not what you want)
for (const key in user) {
if (user.hasOwnProperty(key)) {
console.log(key, user[key]);
}
}
Map
const config = new Map([["theme", "dark"], ["lang", "en"], ["debug", false]]);
// Native iterable — no intermediate array created
for (const [key, value] of config) {
console.log(key, value);
}
// forEach
config.forEach((value, key) => {
console.log(key, value);
});
// Destructured iterators
config.keys(); // MapIterator {"theme", "lang", "debug"}
config.values(); // MapIterator {"dark", "en", false}
config.entries(); // MapIterator {["theme","dark"], ["lang","en"], ...}
The Prototype Trap
const obj = {};
// Looks empty, but...
"toString" in obj; // true — inherited from Object.prototype
"constructor" in obj; // true
"hasOwnProperty" in obj; // true
// This can cause bugs:
const cache = {};
cache["constructor"] = "my value";
// Shadows Object.prototype.constructor — unexpected behavior
// Safe alternative:
const safeObj = Object.create(null); // no prototype chain
"toString" in safeObj; // false
const map = new Map();
// Clean — no prototype pollution
map.has("toString"); // false
map.has("constructor"); // false
// Map keys live in a completely separate namespace
Key Type Flexibility
// Object: keys are coerced to strings
const obj = {};
obj[1] = "one";
obj["1"] = "string one";
console.log(obj[1]); // "string one" — 1 was coerced to "1"
obj[true] = "bool";
console.log(obj["true"]); // "bool" — true was coerced to "true"
const key = { id: 1 };
obj[key] = "object key";
console.log(obj["[object Object]"]); // "object key" — objects become "[object Object]"
// Map: keys keep their original type
const map = new Map();
map.set(1, "number one");
map.set("1", "string one");
console.log(map.get(1)); // "number one"
console.log(map.get("1")); // "string one" — different keys!
const objKey = { id: 1 };
map.set(objKey, "object key");
map.get(objKey); // "object key" — works with reference equality
map.size; // 3
Serialization
// Object: native JSON support
const obj = { name: "Alice", age: 30 };
const json = JSON.stringify(obj); // '{"name":"Alice","age":30}'
const parsed = JSON.parse(json); // { name: "Alice", age: 30 }
// Map: requires manual conversion
const map = new Map([["name", "Alice"], ["age", 30]]);
// Map → JSON
const json = JSON.stringify(Object.fromEntries(map));
// JSON → Map
const restored = new Map(Object.entries(JSON.parse(json)));
// Or for non-string keys:
const json2 = JSON.stringify([...map]); // serialize as array of entries
const restored2 = new Map(JSON.parse(json2));
Performance: When It Matters
Both are O(1) for get/set/delete in Big-O terms, but constant factors differ significantly depending on how many keys you have.
Small collections (< ~30 keys)
Object wins or ties. V8 uses Hidden Classes and Inline Caching — property access is nearly as fast as accessing a field in a C struct.
// V8 internally creates a "hidden class" for this shape:
const user = { name: "Alice", age: 30, role: "admin" };
// Subsequent access is extremely fast because V8 knows
// the exact memory offset of each property.
user.name; // direct offset lookup — no hash needed
Large collections (1000+ dynamic keys)
Map wins. When an Object has too many dynamically-added keys, V8 switches to "dictionary mode" — a hash table that's less optimized than Map's purpose-built one.
const lookup = {};
for (let i = 0; i < 10000; i++) {
lookup[`key_${i}`] = i; // V8 gives up on hidden classes → dictionary mode
}
// At this point, lookup["key_500"] is a hash table access
// Map would be faster here because its hash table has
// less overhead (no prototype chain, no property descriptors)
Frequent add/delete
Map wins clearly. delete obj[key] is one of the slowest operations in V8 because it breaks the hidden class chain and forces the engine to rebuild internal structures.
// This is expensive — don't do this in hot paths:
const cache = {};
cache["session_123"] = data;
delete cache["session_123"]; // breaks hidden class
// This is fast — Map is designed for this:
const cache = new Map();
cache.set("session_123", data);
cache.delete("session_123"); // O(1), no side effects
Benchmark summary (1,000 entries)
| Operation | Winner | Why |
|---|---|---|
| Insert | Map | Designed for dynamic additions |
| Lookup | Map | Object falls into dictionary mode at this size |
| Update | Tie | Both are simple hash table operations |
| Delete | Map |
delete on Object breaks hidden classes |
| Has Key | Map |
map.has() vs in (prototype chain traversal) |
| Iterate | Map | Native iterator vs Object.keys() array allocation |
Try it yourself: Interactive Demo — benchmark Object vs Map with real API data in your own browser.
Decision Flowchart
Do you need JSON serialization?
├── Yes → Object
└── No
└── Are keys always strings?
├── No → Map (only Map supports non-string keys)
└── Yes
└── Fixed shape, known at write time?
├── Yes → Object (V8 optimizes this heavily)
└── No
└── Frequent add/delete?
├── Yes → Map
└── How many keys?
├── < 100 → Object (simpler, good enough)
└── 100+ → Map
Quick Reference
Use Object when:
- Representing a record/struct with fixed fields (
user.name,user.age) - Response/payload objects from APIs (JSON-native)
- You need destructuring (
const { name, age } = user) - Writing config/options objects passed to functions
- Keys are always strings and the set is small and static
Use Map when:
- Building a cache, lookup table, or registry
- Keys are not strings (DOM nodes, objects, numbers)
- You need guaranteed insertion order
- You add and remove entries frequently
- You need
.sizewithout counting - You want to iterate without
Object.keys()overhead - You want no prototype pollution risk
Common Patterns
Pattern: Object as struct, Map as index
interface User {
id: number;
name: string;
email: string;
}
// Object for data shape (struct)
const user: User = { id: 1, name: "Alice", email: "alice@dev.to" };
// Map for lookup index (dictionary)
const usersById = new Map<number, User>();
usersById.set(user.id, user);
// Fast lookup
const found = usersById.get(1); // O(1), type-safe key
Pattern: Map for counting
function wordFrequency(text: string): Map<string, number> {
const freq = new Map<string, number>();
for (const word of text.split(/\s+/)) {
freq.set(word, (freq.get(word) ?? 0) + 1);
}
return freq; // iteration order = insertion order
}
Pattern: Map for caching with object keys
const cache = new Map<HTMLElement, DOMRect>();
function getCachedRect(el: HTMLElement): DOMRect {
if (cache.has(el)) return cache.get(el)!;
const rect = el.getBoundingClientRect();
cache.set(el, rect); // DOM element as key — impossible with Object
return rect;
}
Pattern: WeakMap for private data (no memory leak)
const metadata = new WeakMap<object, { createdAt: number }>();
function track(obj: object) {
metadata.set(obj, { createdAt: Date.now() });
}
// When obj is garbage collected, its WeakMap entry is automatically removed
// This is impossible with Object or Map
If you found this useful, drop a reaction. Questions? Let's discuss in the comments.

Top comments (0)