Symbol
A Symbol is a primitive type, added in ES2015, whose every instance is guaranteed to be unique — no two symbols are ever equal, even if they share the same description. That uniqueness makes symbols ideal for object keys that can never collide with other keys, and the language reserves a handful of “well-known” symbols that let your own objects plug directly into built-in behavior like iteration and type coercion. Unlike strings, symbols are not auto-converted to text, so they stay quietly out of the way of normal property access.
Creating unique symbols
Call Symbol() to mint a brand-new, one-of-a-kind value. The optional string argument is only a description used for debugging — it does not affect identity.
const a = Symbol("id");
const b = Symbol("id");
console.log(a === b); // false — every symbol is unique
console.log(typeof a); // "symbol"
console.log(a.description); // "id"
console.log(a.toString()); // "Symbol(id)"
Output:
false
symbol
id
Symbol(id)
Because symbols are primitives, you never use new Symbol() — that throws a TypeError. Two symbols are equal only when they are literally the same value.
Symbols as object keys
Symbols can be used as property keys alongside strings. Their headline benefit is collision-proofing: a library can attach a symbol key to an object without any risk of clobbering a property the user (or another library) defined. They are also semi-hidden — symbol keys do not show up in for...in, Object.keys(), or JSON.stringify().
const ROLE = Symbol("role");
const user = {
name: "Ada",
[ROLE]: "admin", // computed property syntax
};
console.log(user.name); // "Ada"
console.log(user[ROLE]); // "admin"
// Symbol keys are skipped by the usual enumeration
console.log(Object.keys(user)); // ["name"]
console.log(JSON.stringify(user)); // {"name":"Ada"}
// But they ARE reachable when you ask explicitly
console.log(Object.getOwnPropertySymbols(user)); // [ Symbol(role) ]
Output:
Ada
admin
[ "name" ]
{"name":"Ada"}
[ Symbol(role) ]
Symbol keys are hidden-ish, not private. Anyone can enumerate them with
Object.getOwnPropertySymbols()orReflect.ownKeys(). For true privacy, use class private fields (#field).
The global symbol registry: Symbol.for
Symbol() always returns a fresh value, so you cannot share a symbol across files just by calling it again. Symbol.for(key) solves this with a global registry: the first call creates a symbol, and every later call with the same string key returns that same symbol — even across different modules, iframes, or realms.
const s1 = Symbol.for("app.config");
const s2 = Symbol.for("app.config");
console.log(s1 === s2); // true — same registered symbol
console.log(Symbol.keyFor(s1)); // "app.config"
// A plain Symbol() is not in the registry
const local = Symbol("app.config");
console.log(Symbol.keyFor(local)); // undefined
Output:
true
app.config
undefined
Symbol("x") | Symbol.for("x") | |
|---|---|---|
| Uniqueness | Always a new value | Shared per key |
| Registry | Not registered | Stored globally |
| Cross-realm sharing | No | Yes |
| Recover the key | s.description | Symbol.keyFor(s) |
Use Symbol.for for well-known, intentionally shared identifiers; use plain Symbol() for private, internal keys you do not want anyone else to recreate.
Well-known symbols
The language exposes a set of static “well-known” symbols on the Symbol object. Assigning a method to one of these keys lets your object hook into a built-in operation. The most important is Symbol.iterator, which makes an object usable with for...of and the spread operator.
const range = {
from: 1,
to: 4,
[Symbol.iterator]() {
let current = this.from;
const last = this.to;
return {
next() {
return current <= last
? { value: current++, done: false }
: { value: undefined, done: true };
},
};
},
};
console.log([...range]); // [1, 2, 3, 4]
for (const n of range) console.log(n);
Output:
[ 1, 2, 3, 4 ]
1
2
3
4
Another commonly used hook is Symbol.toPrimitive, which controls how an object is coerced to a primitive in numeric, string, or default contexts.
const money = {
amount: 42,
[Symbol.toPrimitive](hint) {
if (hint === "number") return this.amount;
if (hint === "string") return `$${this.amount}`;
return `Money(${this.amount})`; // hint === "default"
},
};
console.log(+money); // 42 (number hint)
console.log(`${money}`); // "$42" (string hint)
console.log(money + ""); // "Money(42)" (default hint)
Output:
42
$42
Money(42)
Other useful well-known symbols include Symbol.asyncIterator (for for await...of), Symbol.hasInstance (customize instanceof), and Symbol.toStringTag (control the Object.prototype.toString label).
class Stack {
get [Symbol.toStringTag]() {
return "Stack";
}
}
console.log(Object.prototype.toString.call(new Stack())); // [object Stack]
Output:
[object Stack]
Best Practices
- Use
Symbol()for object keys that must never collide with user or library properties, such as internal metadata. - Treat symbol keys as hidden-but-discoverable; reach for class private fields (
#x) when you need genuine privacy. - Reserve
Symbol.forfor identifiers that are deliberately shared across modules or realms, and namespace the key (e.g."mylib.id") to avoid clashes. - Pass a descriptive label to
Symbol("...")— it costs nothing and makes debugging far easier. - Implement
Symbol.iteratorto make custom collections work seamlessly withfor...of, spread, and destructuring. - Remember that
JSON.stringifysilently drops symbol-keyed properties, so do not rely on symbols for data you intend to serialize.