Why Do Object Keys Collide in JavaScript? Uncovering ToPrimitive and Safer Alternatives
This article explains why using objects as JavaScript property keys leads to implicit string conversion collisions, demonstrates the underlying ToPrimitive and ToPropertyKey mechanisms with code examples, and recommends safer alternatives such as Map, Symbol, or explicit unique IDs.
Introduction
In JavaScript, seemingly simple syntax can hide implicit conversion traps. This article starts from a common interview question and step‑by‑step analyzes the ToPrimitive and ToPropertyKey processes that occur when an object is used as a property key, explaining why different references are stringified to the same key and cause overwriting.
Interview Code Example
const a = {};
const b = { key: 'b' };
const c = { key: 'c' };
a[b] = 123;
a[c] = 456;
console.log(a[b]);The intuitive expectation is that the output is 123, but the actual result is 456.
Property Keys Are Only Strings or Symbols
Ordinary object property names can only be strings or Symbols. Non‑string keys are converted via ToPropertyKey (which first applies ToPrimitive) before becoming a property name. Thus different object references may be implicitly stringified to the same key, leading to overwriting or data loss. Use Map, Symbol, or explicit unique IDs to avoid this.
How Objects Are Converted to Strings
When JavaScript converts an object to a string it triggers ToPrimitive, which typically calls valueOf or toString. Most plain objects inherit Object.prototype.toString, returning "[object Object]". Example:
const b = { key: 'b' };
console.log(String(b)); // [object Object]Because both b and c become "[object Object]", they collide as keys.
Back to the Interview Question
Step‑by‑step analysis shows that a[b] and a[c] are equivalent to a[String(b)] and a[String(c)], both yielding "[object Object]". The second assignment overwrites the first, leaving the object equivalent to { "[object Object]": 456 }. Accessing a[b] therefore returns 456.
Use Map for Safer Object Keys
When you need to distinguish objects by reference, replace plain objects with ES6 Map, whose keys can be any type and are compared by reference equality, avoiding string conversion. Example:
const m = new Map();
m.set(b, 123);
m.set(c, 456);
console.log(m.get(b)); // 123
console.log(m.get(c)); // 456Map also provides a clearer API and iteration support, making it preferable for “object indexing” scenarios.
Custom toString as a Workaround
Overriding an object’s toString can produce distinct string keys, allowing objects to act as keys in plain objects. Example:
const a = {};
const b = { toString: () => 'b' };
const c = { toString: () => 'c' };
a[b] = 123;
a[c] = 456;
console.log(a[b]); // 123Although functional in controlled cases, this technique is discouraged because it relies on implicit conversion, reduces readability, and may cause side effects in inheritance, serialization, or third‑party libraries. Prefer Map, Symbol, or explicit unique identifiers.
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.
