Symbols & Well-Known Symbols
Use Symbol primitives as unique, collision-proof property keys and customise built-in JavaScript behaviour with well-known symbols.
Overview
Key Points
- •Symbol() always returns a unique value; two Symbol() calls with the same description are never ===
- •Symbol descriptions (the string argument) are only for debugging they have no semantic meaning
- •Symbol-keyed properties are not enumerable in for-in, Object.keys(), or JSON.stringify()
- •Object.getOwnPropertySymbols() retrieves symbol-keyed properties; Reflect.ownKeys() returns all keys including symbols
- •Symbol.for(key) looks up a global registry, creating the symbol only if it does not exist yet
- •Well-known symbols customise built-in JS behaviour: Symbol.iterator, Symbol.toPrimitive, Symbol.toStringTag, Symbol.hasInstance
- •Symbol.toPrimitive receives a hint ("number", "string", "default") and should return the appropriate primitive
- •Symbols cannot be coerced to strings with + but can be explicitly converted with String(sym) or sym.toString()
Examples
Using Symbols as unique, non-colliding property keys
// Two libraries independently add metadata to the same object
const LIB_A_META = Symbol('lib-a-meta');
const LIB_B_META = Symbol('lib-b-meta');
const user = { name: 'Alice', age: 30 };
user[LIB_A_META] = { permissions: ['read', 'write'] };
user[LIB_B_META] = { theme: 'dark' };
// Normal enumeration does not see symbol keys
console.log(Object.keys(user)); // ['name', 'age']
console.log(JSON.stringify(user)); // {"name":"Alice","age":30}
// Retrieve symbol keys explicitly
console.log(Object.getOwnPropertySymbols(user)); // [Symbol(lib-a-meta), Symbol(lib-b-meta)]
console.log(user[LIB_A_META].permissions); // ['read', 'write']
// Symbol uniqueness
const s1 = Symbol('tag');
const s2 = Symbol('tag');
console.log(s1 === s2); // falseSymbol keys are invisible to JSON.stringify and Object.keys, making them perfect for internal metadata.
Symbol.toPrimitive full control over type coercion
class Money {
constructor(amount, currency) {
this.amount = amount;
this.currency = currency;
}
[Symbol.toPrimitive](hint) {
if (hint === 'number') return this.amount;
if (hint === 'string') return `${this.amount} ${this.currency}`;
// 'default' hint used in + with strings or == comparisons
return this.amount;
}
}
const price = new Money(99.99, 'USD');
console.log(`Price: ${price}`); // Price: 99.99 USD (string hint)
console.log(price * 2); // 199.98 (number hint)
console.log(price + 0.01); // 100 (default hint)
// Without Symbol.toPrimitive, all three would call .valueOf() or toString()The hint argument tells you the context in which coercion is happening so you can return the most appropriate value.
Symbol.hasInstance and Symbol.toStringTag
// Symbol.hasInstance customise instanceof
class EvenNumber {
static [Symbol.hasInstance](value) {
return typeof value === 'number' && value % 2 === 0;
}
}
console.log(4 instanceof EvenNumber); // true
console.log(7 instanceof EvenNumber); // false
console.log(12 instanceof EvenNumber); // true
// Symbol.toStringTag customise Object.prototype.toString output
class HttpResponse {
get [Symbol.toStringTag]() {
return 'HttpResponse';
}
}
const resp = new HttpResponse();
console.log(Object.prototype.toString.call(resp));
// [object HttpResponse]
// Global symbol registry
const KEY = Symbol.for('app.config');
const same = Symbol.for('app.config');
console.log(KEY === same); // true
console.log(Symbol.keyFor(KEY)); // 'app.config'Symbol.hasInstance lets you define what it means to be an "instance of" your class, independent of prototype chains.
Common Mistakes
- Trying to use a symbol as an object key with dot notation symbol keys always require bracket notation: obj[sym]
- Assuming Symbol.for() is the same as Symbol() Symbol.for() uses a shared global registry and may return an existing symbol
- Forgetting that symbol-keyed properties survive Object.assign and the spread operator ({...obj}) they are copied, just not enumerated
- Using + to concatenate a symbol with a string this throws a TypeError; use String(sym) or template literals explicitly
Note
Use Symbol.for() to share symbols across ES modules without explicit imports two modules that both call Symbol.for("myapp.event") get the exact same symbol. This is how libraries can agree on a shared protocol without coupling to each other's exports.
Exercises
Write JavaScript code to solve each exercise. Run it and check your output.
Completed: 0 / 5 exercises
Knowledge Check
Question 1 of 4