JavaScript Symbols and Well-Known Symbols
Master JavaScript Symbols for unique property keys, explore well-known symbols like Symbol.iterator and Symbol.toPrimitive, and learn metaprogramming patterns.
What you'll learn
- ✓What Symbols are and why they exist
- ✓How well-known symbols customize built-in JavaScript behavior
- ✓Practical uses for Symbol.iterator, Symbol.toPrimitive, and Symbol.hasInstance
Prerequisites
- •JavaScript objects and property access
- •Basic understanding of iterators
Symbol is a primitive type introduced in ES2015. Every symbol is unique and immutable, making symbols ideal for property keys that will never collide with other keys — even if they share the same description.
Creating Symbols
const sym1 = Symbol("id");
const sym2 = Symbol("id");
console.log(sym1 === sym2); // false -- every Symbol is unique
console.log(typeof sym1); // "symbol"
The string "id" is just a description for debugging. It does not affect uniqueness.
Symbols as property keys
const id = Symbol("id");
const user = {
name: "Alice",
[id]: 12345,
};
console.log(user[id]); // 12345
console.log(user.id); // undefined -- dot notation does not work with symbols
console.log(Object.keys(user)); // ["name"] -- symbols are not enumerable
Symbol-keyed properties are invisible to for...in, Object.keys(), and JSON.stringify(). This makes them useful for attaching metadata without interfering with normal property enumeration.
Accessing symbol properties
// Get symbol-keyed properties specifically
console.log(Object.getOwnPropertySymbols(user)); // [Symbol(id)]
// Or get everything
console.log(Reflect.ownKeys(user)); // ["name", Symbol(id)]
The global symbol registry
Symbol.for() creates shared symbols. If a symbol with the given key already exists in the global registry, it returns the existing one.
const s1 = Symbol.for("app.id");
const s2 = Symbol.for("app.id");
console.log(s1 === s2); // true -- same symbol from the registry
// Retrieve the key from a registered symbol
console.log(Symbol.keyFor(s1)); // "app.id"
// Regular symbols are not registered
const s3 = Symbol("app.id");
console.log(Symbol.keyFor(s3)); // undefined
Global symbols are useful when multiple parts of an application (or multiple libraries) need to agree on the same symbol.
Well-Known Symbols
JavaScript defines several built-in symbols that let you customize how objects interact with language features. These are properties of the Symbol constructor.
Symbol.iterator
Defines the default iterator for an object, enabling for...of loops and spread syntax.
class Range {
constructor(start, end) {
this.start = start;
this.end = end;
}
[Symbol.iterator]() {
let current = this.start;
const end = this.end;
return {
next() {
if (current <= end) {
return { value: current++, done: false };
}
return { done: true };
},
};
}
}
const range = new Range(1, 5);
for (const n of range) {
console.log(n); // 1, 2, 3, 4, 5
}
console.log([...range]); // [1, 2, 3, 4, 5]
const [first, second] = range;
console.log(first, second); // 1 2
Symbol.toPrimitive
Controls how an object is converted to a primitive value. Receives a hint of "number", "string", or "default".
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 by == and +
return this.amount;
}
}
const price = new Money(29.99, "USD");
console.log(`Price: ${price}`); // "Price: 29.99 USD" (string hint)
console.log(price + 10); // 39.99 (default hint)
console.log(+price); // 29.99 (number hint)
console.log(price > 20); // true (number hint)
Symbol.hasInstance
Customizes the behavior of the instanceof operator.
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("4" instanceof EvenNumber); // false
Symbol.toStringTag
Controls the string returned by Object.prototype.toString().
class Database {
get [Symbol.toStringTag]() {
return "Database";
}
}
const db = new Database();
console.log(Object.prototype.toString.call(db)); // "[object Database]"
console.log(`${db}`); // Still calls toString(), which you can also override
Symbol.species
Determines the constructor used when built-in methods create derived objects. This is important when subclassing built-in types.
class TrackedArray extends Array {
// Ensure methods like .map() and .filter() return plain Arrays
static get [Symbol.species]() {
return Array;
}
}
const tracked = new TrackedArray(1, 2, 3);
const mapped = tracked.map((x) => x * 2);
console.log(mapped instanceof TrackedArray); // false
console.log(mapped instanceof Array); // true
Without Symbol.species, mapped would be a TrackedArray, which might not be desired if the subclass has expensive construction logic.
Symbol.asyncIterator
Enables for await...of loops for asynchronous iteration.
class AsyncRange {
constructor(start, end) {
this.start = start;
this.end = end;
}
[Symbol.asyncIterator]() {
let current = this.start;
const end = this.end;
return {
async next() {
// Simulate async work
await new Promise((r) => setTimeout(r, 100));
if (current <= end) {
return { value: current++, done: false };
}
return { done: true };
},
};
}
}
async function main() {
for await (const n of new AsyncRange(1, 3)) {
console.log(n); // 1, 2, 3 (with 100ms delay between each)
}
}
main();
Practical patterns
Preventing property name collisions in libraries
// Library code
const INTERNAL_STATE = Symbol("internalState");
function enhance(obj) {
obj[INTERNAL_STATE] = { initialized: true, version: 2 };
return obj;
}
function isEnhanced(obj) {
return obj[INTERNAL_STATE]?.initialized === true;
}
// User code -- no risk of collision
const myObj = { internalState: "user data" };
enhance(myObj);
console.log(myObj.internalState); // "user data" -- untouched
console.log(isEnhanced(myObj)); // true
Type branding with symbols
const Validated = Symbol("validated");
function validateEmail(email) {
if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email)) {
throw new Error("Invalid email");
}
const branded = new String(email);
branded[Validated] = true;
return branded;
}
function sendEmail(to) {
if (!to[Validated]) {
throw new Error("Email must be validated first");
}
// Safe to send
}
Summary
Symbols provide guaranteed-unique property keys and a mechanism for customizing JavaScript’s built-in behaviors:
- Every
Symbol()call creates a unique value, preventing property name collisions. Symbol.for()creates shared symbols via a global registry.- Symbol-keyed properties are hidden from
Object.keys(),for...in, andJSON.stringify(). - Well-known symbols like
Symbol.iterator,Symbol.toPrimitive, andSymbol.hasInstancelet you control how objects interact with language operators and built-in methods. - Libraries use symbols to attach internal metadata to objects without risking collisions with user-defined properties.
Symbols are one of JavaScript’s most underused features. Understanding them unlocks powerful metaprogramming patterns and cleaner library design.
Related articles
- JavaScript JavaScript Proxy and Reflect: Metaprogramming Guide
Master JavaScript Proxy and Reflect API for metaprogramming with practical examples including validation, observable objects, and access control patterns.
- JavaScript Generators, Iterators, and Symbol.iterator in JavaScript
Master JavaScript generators and iterators -- learn function*, yield, Symbol.iterator, and how to build custom iterable objects.
- JavaScript Proxy and Reflect API for JavaScript Metaprogramming
Learn how to use JavaScript Proxy and Reflect to intercept operations, build validators, create observable objects, and write metaprogramming patterns.
- JavaScript Iterators and Generators in JavaScript
A deep dive into JavaScript iterators and generators: the iterator protocol, Symbol.iterator, generator functions, yield, async generators, and practical patterns for lazy evaluation and data pipelines.