Well-Known Symbols
JavaScript has built-in Symbols that customize object behavior. These are called well-known Symbols. They allow you to hook into language mechanics like iteration, type conversion, and string representation.
Custom Iteration with Symbol.iterator
Makes an object iterable with for...of:
const range = {
start: 1,
end: 5,
[Symbol.iterator]() {
let current = this.start;
const end = this.end;
return {
next() {
if (current <= end) {
return { value: current++, done: false };
}
return { done: true };
},
};
},
};
for (const num of range) {
console.log(num); // 1, 2, 3, 4, 5
}
console.log([...range]); // [1, 2, 3, 4, 5]
Customizing String Tags with Symbol.toStringTag
Customizes the Object.prototype.toString() result:
class MyClass {
get [Symbol.toStringTag]() {
return "MyClass";
}
}
const obj = new MyClass();
console.log(Object.prototype.toString.call(obj)); // "[object MyClass]"
Controlling Type Conversion with Symbol.toPrimitive
const money = {
amount: 100,
currency: "USD",
[Symbol.toPrimitive](hint) {
if (hint === "number") {
return this.amount;
}
if (hint === "string") {
return `${this.amount} ${this.currency}`;
}
return this.amount; // default
},
};
console.log(+money); // 100 (number hint)
console.log(`${money}`); // "100 USD" (string hint)
console.log(money + 50); // 150 (default hint)
Other Well-Known Symbols
| Symbol | Purpose |
|---|---|
Symbol.hasInstance |
Customize instanceof behavior |
Symbol.isConcatSpreadable |
Control array spreading in concat() |
Symbol.match |
Make object work with String.match() |
Symbol.replace |
Make object work with String.replace() |
Symbol.search |
Make object work with String.search() |
Symbol.split |
Make object work with String.split() |
Symbol.species |
Constructor for derived objects |
Symbol.unscopables |
Exclude properties from with binding |
Practical Use Cases
Avoiding Property Name Collisions
When adding properties to objects you do not control:
// Library adds metadata without risking collision
const metadata = Symbol("metadata");
function addMetadata(obj, data) {
obj[metadata] = data;
}
function getMetadata(obj) {
return obj[metadata];
}
const userObj = { name: "Bob" };
addMetadata(userObj, { timestamp: Date.now() });
// User's properties are safe
console.log(Object.keys(userObj)); // ["name"]
console.log(getMetadata(userObj)); // { timestamp: ... }
Defining Constants
Symbols guarantee unique values for constants:
const Status = {
PENDING: Symbol("pending"),
APPROVED: Symbol("approved"),
REJECTED: Symbol("rejected"),
};
function processRequest(status) {
switch (status) {
case Status.PENDING:
return "Waiting...";
case Status.APPROVED:
return "Done!";
case Status.REJECTED:
return "Failed";
}
}
// Can't accidentally match with a string
console.log(processRequest("pending")); // undefined
console.log(processRequest(Status.PENDING)); // "Waiting..."
Private-ish Properties
This is not real privacy. But a property keyed by a Symbol is not reachable from code that does not have a reference to that same Symbol:
const _count = Symbol("count");
class Counter {
constructor() {
this[_count] = 0;
}
increment() {
this[_count]++;
}
get value() {
return this[_count];
}
}
const counter = new Counter();
counter.increment();
console.log(counter.value); // 1
console.log(counter._count); // undefined (not accessible by string)
Summary
- Symbols are unique, immutable primitive values
- Use them for property keys that will not collide with other keys
- Symbol properties are hidden from normal enumeration
- Well-known Symbols customize JavaScript’s built-in behaviors
- The global registry (
Symbol.for()) allows sharing Symbols across code