Symbol: Unique Keys & Well-Known Symbols
Symbol kabhi use nahi hota? Galat bhai — har for…of mein use hota hai.
Symbol() creates a primitive value guaranteed to be unique — forever. Every call to Symbol() returns a value that has never existed before and will never exist again.
Two calls to Symbol() ALWAYS return different values — even with the same description string. The description is just a label for debugging, not part of the identity.
typeof Symbol() returns 'symbol' — the seventh and final primitive type in JavaScript (after string, number, bigint, boolean, undefined, null).
Symbols are NOT auto-converted to strings. Trying '' + Symbol('id') throws a TypeError. You must use .toString() or String() for explicit conversion.
// Each Symbol() call = new unique value
const id1 = Symbol('id');
const id2 = Symbol('id');
console.log(id1 === id2); // false — always unique!
console.log(typeof id1); // 'symbol'
console.log(id1.toString()); // 'Symbol(id)' — explicit conversion
console.log(id1.description); // 'id' — the description string
// As object property key
const user = {
name: 'Sai',
[id1]: 12345, // symbol as computed property key
};
console.log(user[id1]); // 12345
console.log(user.name); // 'Sai'
// Symbols are "hidden" from common enumeration
console.log(Object.keys(user)); // ['name'] — no symbol!
console.log(JSON.stringify(user)); // '{"name":"Sai"}' — no symbol!
for (const key in user) {
console.log(key); // only 'name'
}
// But NOT truly private:
const syms = Object.getOwnPropertySymbols(user);
console.log(user[syms[0]]); // 12345 — accessible!
Symbol.for('key') looks up or creates a symbol in the global symbol registry. If the registry already has an entry for 'key', it returns the exact same symbol. If not, it creates one, stores it, and returns it.
This means Symbol.for('key') === Symbol.for('key') is always true — across any module, any file, any scope.
Symbol.keyFor(sym) does the reverse: given a symbol from the global registry, it returns the string key used to create it. For local symbols (created with Symbol()), it returns undefined.
// Symbol.for — global registry
const sym1 = Symbol.for('app.config');
const sym2 = Symbol.for('app.config');
console.log(sym1 === sym2); // true — same registry entry!
// vs regular Symbol:
const local1 = Symbol('id');
const local2 = Symbol('id');
console.log(local1 === local2); // false — different each time
// Reverse lookup
const key = Symbol.keyFor(sym1);
console.log(key); // 'app.config'
const localSym = Symbol('local');
console.log(Symbol.keyFor(localSym)); // undefined — not in registry
// Cross-module sharing without import
// In module A:
const HANDLER = Symbol.for('app.clickHandler');
element[HANDLER] = function() { console.log('clicked!'); };
// In module B (no import of A needed):
const HANDLER = Symbol.for('app.clickHandler'); // same symbol!
element[HANDLER](); // works! 'clicked!'
// Use namespaced keys to avoid collisions:
Symbol.for('mylib.render') // not just 'render'
Symbol.for('mylib.update') // not just 'update'
• Use Symbol.for() when you need to share a symbol across different files or modules without a direct import/export.
• Use Symbol() (no for) when you want a truly unique key that no other code can accidentally access — for private-ish data or non-enumerable metadata.
Well-known symbols are predefined Symbol values on the Symbol object itself. They define "hooks" that V8 calls internally when performing built-in JavaScript operations.
Symbol.iterator: defines how an object is iterated (for...of, spread).
Symbol.toPrimitive: defines how an object converts to a primitive.
Symbol.hasInstance: defines instanceof behavior.
Symbol.species: defines which constructor is used for derived objects.
Symbol.toStringTag: defines what Object.prototype.toString returns.
These are how you CUSTOMIZE built-in JS behavior for your objects.
// Symbol.iterator — making your object iterable
const range = {
from: 1,
to: 5,
[Symbol.iterator]() {
let current = this.from;
const last = this.to;
return {
next() {
return current <= last
? { value: current++, done: false }
: { value: undefined, done: true };
}
};
}
};
for (const n of range) { console.log(n); } // 1, 2, 3, 4, 5
console.log([...range]); // [1, 2, 3, 4, 5]
const [a, b, c] = range; // destructuring works too!
// Symbol.toPrimitive — control type coercion
const temperature = {
celsius: 100,
[Symbol.toPrimitive](hint) {
if (hint === 'number') return this.celsius;
if (hint === 'string') return this.celsius + '°C';
return this.celsius; // 'default' hint
}
};
console.log(+temperature); // 100 — number hint
console.log(`${temperature}`); // '100°C' — string hint
console.log(temperature + 0); // 100 — default hint
// Symbol.toStringTag — custom [object Type]
class MyCollection {
get [Symbol.toStringTag]() { return 'MyCollection'; }
}
const mc = new MyCollection();
console.log(Object.prototype.toString.call(mc)); // '[object MyCollection]'
Every time for...of runs: V8 calls obj[Symbol.iterator]() to get an iterator object. That iterator must have a .next() method returning {value, done}.
Built-in iterables already implement this: Array, String, Map, Set, NodeList, arguments, generators.
You can make ANY object iterable by implementing [Symbol.iterator]. This makes it work with for...of, spread, destructuring, and Array.from().
Infinite iterables: iterators that never set done: true. You MUST use break to stop them.
// Arrays are iterable — Symbol.iterator is why
const arr = [10, 20, 30];
const iter = arr[Symbol.iterator](); // get the iterator
console.log(iter.next()); // {value: 10, done: false}
console.log(iter.next()); // {value: 20, done: false}
console.log(iter.next()); // {value: 30, done: false}
console.log(iter.next()); // {value: undefined, done: true}
// Strings are iterable — handles surrogate pairs correctly
for (const char of '🙂Hi') {
console.log(char); // '🙂', 'H', 'i' — full code points!
}
// Custom infinite iterable:
const naturals = {
[Symbol.iterator]() {
let n = 1;
return {
next() { return { value: n++, done: false }; }
};
}
};
const first5 = [];
for (const n of naturals) {
first5.push(n);
if (n >= 5) break; // MUST use break — never done!
}
console.log(first5); // [1, 2, 3, 4, 5]
// Spread and destructuring both use Symbol.iterator
console.log([...naturals].slice(0, 3)); // wait — infinite spread = hang!
// NEVER spread an infinite iterable! Use take() pattern or break
Symbol keys are hidden from enumeration — this makes them perfect for attaching internal data to objects without polluting their property list.
Library metadata: attach internal data to user objects. Plugin systems: each plugin registers its data under a unique symbol — no key conflicts ever.
React uses Symbol.for('react.element') to mark virtual DOM elements. Node.js uses Symbol.for('nodejs.rejection') for unhandled rejection data.
The pattern: put symbols in a module's closure — only that module can access the property.
// Library metadata without polluting user object
const INTERNAL = Symbol('internal');
const VERSION = Symbol('version');
function createWidget(config) {
const widget = { ...config }; // user's properties
widget[INTERNAL] = { // library metadata — hidden!
createdAt: Date.now(),
id: Math.random().toString(36).slice(2),
};
widget[VERSION] = '2.0.0';
return widget;
}
const w = createWidget({ color: 'blue', size: 'lg' });
console.log(Object.keys(w)); // ['color', 'size'] — clean!
console.log(JSON.stringify(w)); // '{"color":"blue","size":"lg"}' — clean!
console.log(w[INTERNAL].id); // accessible if you have the symbol
// Plugin system — guaranteed no key conflicts
const Plugin1 = (() => {
const DATA = Symbol('plugin1.data');
return {
install(host) { host[DATA] = { initialized: true }; },
getData(host) { return host[DATA]; }
};
})();
const Plugin2 = (() => {
const DATA = Symbol('plugin2.data'); // different symbol — no conflict
return {
install(host) { host[DATA] = { theme: 'dark' }; },
getData(host) { return host[DATA]; }
};
})();
const host = {};
Plugin1.install(host);
Plugin2.install(host);
console.log(Plugin1.getData(host)); // {initialized: true} ✅
console.log(Plugin2.getData(host)); // {theme: 'dark'} ✅
// No key collision even though both use 'DATA' — symbols are unique!
Lo kar liya — Key Points:
- ✅ Symbol() creates a forever-unique primitive — two Symbol('id') calls are never equal
- ✅ Symbol.for('key') uses a global registry — same key always returns same symbol — cross-module sharing
- ✅ Well-known symbols (Symbol.iterator, Symbol.toPrimitive) are hooks V8 calls during built-in operations
- ✅ Symbol.iterator makes any object work with for...of, spread, destructuring, Array.from()
- ✅ Symbol.toPrimitive gives complete control over how your object converts to number, string, or default
- ✅ Symbol keys are hidden from Object.keys(), JSON.stringify(), for...in — ideal for library metadata
Want to track your progress?
Log in to save your place and pick up where you left off.
Progress track karna chahte ho?
Login karo apni progress save karne ke liye aur jahan chhoda tha wahan se shuru karo.
Login