Symbols and well-known symbols

JavaScript · Advanced · 10 min

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 by Object.prototype.toString, e.g. [object Money].
  • Symbol.hasInstance — customise instanceof.
  • Symbol.dispose / Symbol.asyncDispose — used by the newer using declarations 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.ownKeys exposes them.

Check yourself

  1. Which call lists a symbol-keyed property of obj?

Sign in to answer, see the explanations, and get this lesson back as a quick review later.

Official docs: developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Symbol