Technology · JavaScript
AdvancedJavaScript Symbols
Use unique Symbol values as collision-free object keys and customize built-in object behavior.
TL;DR
- 01Every Symbol() call creates a guaranteed-unique primitive value.
- 02Symbols make non-colliding object keys hidden from normal enumeration.
- 03Well-known symbols let objects customize core language behavior.
What Symbols Are
Symbol()creates a new primitive value that is guaranteed unique, even with an identical description.const a = Symbol('id'); const b = Symbol('id'); a === b; // false- Symbols are a primitive type alongside string, number, boolean, undefined, null, and bigint.
- The optional description passed to
Symbol()is only for debugging and doesn't affect identity.const s = Symbol('debug label'); s.toString(); // "Symbol(debug label)" s.description; // "debug label" - Calling
Symbol()withnewthrows a TypeError, since symbols aren't constructible. - Each symbol exists as its own independent value, with no relationship to symbols of the same description.
- Typeof a symbol returns the string
"symbol".
Symbols as Object Keys
- Use a symbol as a property key to guarantee it never collides with any other key.
const ID = Symbol('id'); const user = { name: 'Ada', [ID]: 42 }; user[ID]; // 42 - Access a symbol-keyed property with bracket notation, since dot notation can't reference a variable.
- Symbol keys are useful for adding metadata to objects without risking a clash with existing or future string keys.
const TAG = Symbol('internal-tag'); function tag(obj, value) { obj[TAG] = value; return obj; } - Symbol-keyed properties still show up in
Object.getOwnPropertyDescriptors()andReflect.ownKeys().Reflect.ownKeys(user); // ['name', Symbol(id)] - Symbols don't make a property truly private — anyone holding the symbol reference can read it.
- Mixing symbol and string keys on the same object is common for separating public data from internal flags.
Symbols Are Skipped By Enumeration
for...inandObject.keys()ignore symbol-keyed properties entirely.const obj = { name: 'Ada', [Symbol('id')]: 1 }; Object.keys(obj); // ['name'] for (const key in obj) console.log(key); // logs 'name' onlyJSON.stringify()also drops symbol-keyed properties from the resulting JSON string.JSON.stringify(obj); // '{"name":"Ada"}'- This makes symbols a safe place for metadata that shouldn't leak into serialized output or logs.
- Use
Object.getOwnPropertySymbols()when you explicitly need to list an object's symbol keys.Object.getOwnPropertySymbols(obj); // [Symbol(id)] - Object spread (
{...obj}) does copy symbol-keyed own properties, unlikeJSON.stringify. Object.assign()also copies enumerable symbol properties between objects.
Well-Known Symbols
- Well-known symbols are built-in symbols that let objects hook into core language behavior.
const range = { from: 1, to: 3, [Symbol.iterator]() { let cur = this.from; const last = this.to; return { next: () => cur <= last ? { value: cur++, done: false } : { done: true } }; } }; [...range]; // [1, 2, 3] Symbol.iteratormakes an object work withfor...of, spread syntax, and destructuring.Symbol.asyncIteratordoes the same forfor await...ofloops over asynchronous data sources.const asyncRange = { async *[Symbol.asyncIterator]() { yield 1; yield 2; } };Symbol.toPrimitivecontrols how an object converts to a number, string, or default value.const money = { amount: 50, [Symbol.toPrimitive](hint) { return hint === 'string' ? `$${this.amount}` : this.amount; } }; `${money}`; // "$50" money + 10; // 60Symbol.hasInstancecustomizes howinstanceofchecks behave for a class or object.class Even { static [Symbol.hasInstance](num) { return Number.isInteger(num) && num % 2 === 0; } } 4 instanceof Even; // true
The Global Symbol Registry
Symbol.for(key)looks up a symbol in a global registry, creating one if it doesn't exist yet.const a = Symbol.for('app.id'); const b = Symbol.for('app.id'); a === b; // true- Unlike
Symbol(), callingSymbol.for()with the same key always returns the same shared symbol. - The registry is shared across realms, like the main page and an iframe, in browser environments.
Symbol.keyFor(sym)returns the registry key for a given registered symbol, orundefinedif unregistered.Symbol.keyFor(a); // "app.id" Symbol.keyFor(Symbol('local')); // undefined- Use the global registry when independent modules need to coordinate on the exact same symbol value.
- Prefer plain
Symbol()for most cases — reach for the registry only when cross-module sharing is required.
Tips
- 01Use a symbol property when you need a key that won't collide with other keys sharing a description.
- 02Reach for Symbol.for() when multiple modules or realms need to share the exact same symbol value reliably.
Warnings
- 01Symbol() is never called with new, since attempting that throws a TypeError because symbols aren't constructible objects.
- 02Symbol-keyed properties are skipped by JSON.stringify, for...in, and Object.keys, so they vanish from typical serialization.
- 03Two symbols created with the same description are still completely different values and never compare as equal.
FAQ
- A Symbol is a primitive value that is always unique, even when two symbols share the same description. It's mainly used as an object property key that won't collide with string keys or other symbols. JavaScript also uses symbols internally for well-known protocol hooks like Symbol.iterator.
- JSON.stringify only serializes string-keyed, enumerable own properties by design. Symbol-keyed properties are intentionally excluded, along with functions and undefined values. This makes symbols a good fit for metadata you don't want accidentally serialized or logged.
- Symbol() always creates a brand-new, unique symbol, even if you pass the same description twice. Symbol.for() checks a global registry first and returns an existing symbol if one was already registered under that key. Use Symbol.for() when separate parts of an app, or separate realms like iframes, need to share the same symbol.
- Well-known symbols are built-in symbols like Symbol.iterator that let your objects plug into core language behavior. Implementing Symbol.iterator makes an object work with for...of and spread syntax. Implementing Symbol.toPrimitive controls how an object converts to a number or string.
- Yes, hidden from enumeration doesn't mean private. Use Object.getOwnPropertySymbols() to list all symbol keys on an object directly. Anyone with a reference to the same symbol can also read or write that property normally.