Symbols are how the language exposes hooks like iteration and type conversion to your own objects.
A symbol is a primitive whose only property is being unique. Symbol("id") creates a new one each time; the description is just a label for debugging, so Symbol("id") !== Symbol("id").
Used as property keys, symbols can never clash with string keys or with symbols from other code:
const meta = Symbol("meta");
obj[meta] = { cachedAt: Date.now() };Symbol-keyed properties are skipped by for...in, Object.keys, Object.entries and JSON.stringify. They're not private — Object.getOwnPropertySymbols(obj) and Reflect.ownKeys(obj) reveal them — but they stay out of the way of normal code. That makes them good for library metadata and for extending objects you don't own. For real privacy, use #private class fields.
Symbol.for("app.key") is the exception to uniqueness: it looks up a global registry, returning the same symbol for the same string everywhere in the agent, even across realms such as iframes. Symbol.keyFor(sym) gives the key back.
Well-known symbols are predefined symbols the language checks to let objects customise built-in behaviour:
Symbol.iterator/Symbol.asyncIterator— make an object (async) iterable.Symbol.toPrimitive— control conversion to number or string (+obj, template literals).Symbol.toStringTag— the name shown byObject.prototype.toString, e.g.[object Money].Symbol.hasInstance— customiseinstanceof.Symbol.dispose/Symbol.asyncDispose— used by the newerusingdeclarations for automatic resource cleanup, already shipping in recent V8-based runtimes.
Implementing these protocols makes your objects feel native: they work with spread, template literals and for...of like built-ins do.
Example
const secret = Symbol("secret");
const user = { name: "Ada", [secret]: "token-123" };
console.log(Object.keys(user)); // [ 'name' ]
console.log(JSON.stringify(user)); // {"name":"Ada"}
console.log(Reflect.ownKeys(user)); // [ 'name', Symbol(secret) ]
console.log(Symbol("x") === Symbol("x"), Symbol.for("x") === Symbol.for("x")); // false true
class Money {
constructor(cents, currency) { this.cents = cents; this.currency = currency; }
[Symbol.toPrimitive](hint) {
return hint === "number" ? this.cents / 100 : `${(this.cents / 100).toFixed(2)} ${this.currency}`;
}
get [Symbol.toStringTag]() { return "Money"; }
}
const price = new Money(1999, "EUR");
console.log(`${price}`); // 19.99 EUR
console.log(+price * 2); // 39.98
console.log(Object.prototype.toString.call(price)); // [object Money]Remember
- Every
Symbol()is unique;Symbol.for()shares symbols via a global registry - Symbol keys are hidden from keys/entries/JSON but not truly private
- Well-known symbols (
iterator,toPrimitive,toStringTag) hook into language behaviour
Watch out for
- Treating symbol properties as private data —
Reflect.ownKeysexposes them.