JavaScript Symbol Type: Complete Guide
Symbol is a unique, immutable primitive type in JavaScript. Learn what symbols are, why they exist, and how to use them for private property keys, well-known symbols, and metaprogramming.
A Symbol is a primitive value that is guaranteed to be unique. No two symbols are ever equal, even if they have the same description:
const a = Symbol("id");
const b = Symbol("id");
console.log(a === b); // false -- every symbol is unique
console.log(a.description); // "id"The description is a label for debugging. It does not affect the symbol's identity. Think of it as a comment attached to an otherwise anonymous value.
Symbols are the seventh and most recent primitive type in JavaScript, joining string, number, bigint, boolean, undefined, and null. They were added in ES2015 (ES6).
Why Symbols Exist
Before symbols, every object property key was a string. This created two problems:
- Name collisions. If a library added a property called id to objects, and your code also used id, one would overwrite the other.
- No private-by-convention mechanism. Prefixing with an underscore was a convention, not enforcement.
Symbols solve both. A symbol-keyed property can never collide with a string-keyed property or with another symbol.
Using Symbols as Property Keys
You use symbols as object keys with bracket notation:
const userId = Symbol("id");
const userName = Symbol("name");
const user = {
[userId]: 42,
[userName]: "Alex",
name: "Alex" // This is a regular string key -- separate from the symbol
};
console.log(user[userId]); // 42
console.log(user.name); // "Alex" -- the string key
console.log(user[userName]); // "Alex" -- the symbol key, same value, different keyThe object now has three properties: a string-keyed "name" and two symbol-keyed properties. They never interfere with each other.
Symbol-keyed properties are hidden from most enumeration:
console.log(Object.keys(user)); // ["name"] -- only string keys
console.log(Object.getOwnPropertyNames(user)); // ["name"]
console.log(Object.getOwnPropertySymbols(user)); // [Symbol(id), Symbol(name)]
// for...in also skips symbols
for (const key in user) {
console.log(key); // "name" only
}This makes symbols ideal for attaching metadata or internal state to objects without cluttering the public interface.
Well-Known Symbols
JavaScript defines a set of built-in symbols that let you customize how objects behave with language features. These are called well-known symbols:
| Symbol | Customizes |
|---|---|
Symbol.iterator | How the object works with for...of and spread |
Symbol.asyncIterator | How the object works with for await...of |
Symbol.toPrimitive | How the object converts to a primitive (for addition, String conversion, etc.) |
Symbol.toStringTag | What Object.prototype.toString() returns |
Symbol.hasInstance | How instanceof decides if an object is an instance |
Symbol.species | Which constructor to use for derived objects |
Symbol.isConcatSpreadable | Whether the object spreads in Array.prototype.concat() |
| Symbol.match / Symbol.replace / Symbol.search / Symbol.split | Custom regex-like behavior for string methods |
Symbol.iterator -- Making Objects Iterable
The most commonly used well-known symbol is Symbol.iterator. Define it on any object to make it work with for...of:
const range = {
from: 1,
to: 5,
[Symbol.iterator]() {
let current = this.from;
const last = this.to;
return {
next() {
if (current <= last) {
return { value: current++, done: false };
}
return { value: undefined, done: true };
}
};
}
};
for (const num of range) {
console.log(num); // 1, 2, 3, 4, 5
}
console.log([...range]); // [1, 2, 3, 4, 5]The Symbol.iterator method must return an object with a next() method, and that method must return a value/done object. This is the iterator protocol, covered in full in /javascript/javascript-iterators-complete-guide.
Generators, covered in /javascript/javascript-generators-deep-dive-full-guide, are a concise way to build iterator functions.
Symbol.toPrimitive -- Controlling Type Coercion
When JavaScript needs to convert an object to a primitive (for example, when you use addition or String conversion), it calls Symbol.toPrimitive:
const money = {
amount: 49.99,
currency: "USD",
[Symbol.toPrimitive](hint) {
if (hint === "number") return this.amount;
if (hint === "string") return `${this.currency} ${this.amount}`;
return this.amount; // default
}
};
console.log(+money); // 49.99 (number hint)
console.log(`${money}`); // "USD 49.99" (string hint)
console.log(money + 10); // 59.99 (default hint → number)The hint parameter tells you the context: "number" for math operations, "string" for string interpolation, and "default" when the engine is unsure.
Symbol.toStringTag -- Custom toString Output
By default, Object.prototype.toString.call(obj) returns "[object Object]". Override Symbol.toStringTag to return a custom label:
class ApiResponse {
get [Symbol.toStringTag]() {
return "ApiResponse";
}
}
const res = new ApiResponse();
console.log(Object.prototype.toString.call(res)); // "[object ApiResponse]"This is used by libraries and frameworks that check the type tag for duck-typing. It does not affect the typeof operator.
The Global Symbol Registry
Symbol.for() looks up or creates a symbol in a global registry that spans all realms, such as iframes and web workers:
const shared1 = Symbol.for("app.config");
const shared2 = Symbol.for("app.config");
console.log(shared1 === shared2); // true -- same symbol from registry
// Get the key back
console.log(Symbol.keyFor(shared1)); // "app.config"
// Symbols created directly are not in the registry
const local = Symbol("app.config");
console.log(Symbol.keyFor(local)); // undefinedUse Symbol.for() when you need the same symbol value across different execution contexts, such as an iframe and its parent page, or a shared library module. In normal application code, plain Symbol() is almost always sufficient and safer.
Practical Use Cases
Enums with guaranteed uniqueness
const Status = {
IDLE: Symbol("idle"),
LOADING: Symbol("loading"),
SUCCESS: Symbol("success"),
ERROR: Symbol("error")
};
function handleStatus(status) {
switch (status) {
case Status.LOADING:
return "Loading...";
case Status.SUCCESS:
return "Done!";
default:
return "Unknown";
}
}
console.log(handleStatus(Status.SUCCESS)); // "Done!"String enums like Status.LOADING = "loading" work too, but symbols guarantee that no external input or string value can accidentally match. Status.LOADING will never equal any string, ever.
Hiding internal state in a class
const _counter = Symbol("counter");
class ClickTracker {
constructor() {
this[_counter] = 0;
}
click() {
this[_counter] += 1;
console.log(`Clicked ${this[_counter]} times`);
}
}
const tracker = new ClickTracker();
tracker.click(); // Clicked 1 times
tracker.click(); // Clicked 2 times
// External code cannot accidentally access or overwrite _counter
console.log(Object.keys(tracker)); // [] -- no public counter propertyNote: symbols do not provide true privacy. Object.getOwnPropertySymbols(tracker) still reveals them, so symbols provide collision-avoidance, not access control.
For true private fields, use the # private field syntax added in ES2022.
Symbols vs Strings vs Private Fields
| Feature | String keys | Symbol keys | Private fields |
|---|---|---|---|
| Enumerable? | Yes | No | No |
| Collision with other code? | Possible | Impossible | Impossible |
| Accessible via reflection? | Yes | Yes (getOwnPropertySymbols) | No |
| In JSON? | Yes | No | No |
| Accessible from outside the class? | Yes | Yes | No |
| Syntax | obj.key or obj["key"] | obj[symbolVar] | this.#field |
Symbols sit between string keys (fully public) and private fields (fully inaccessible). Use strings for public API properties, symbols for collision-free metadata, and private fields for true encapsulation.
Rune AI
Key Insights
- Symbol is a primitive type whose values are always unique, even with the same description.
- Symbols are used as property keys that cannot collide with string keys or other symbols.
- Well-known symbols (Symbol.iterator, Symbol.toPrimitive, etc.) define built-in object behaviors.
- Symbol.for() and Symbol.keyFor() provide a global symbol registry for cross-realm sharing.
- Symbols are not enumerable in for...in and are ignored by JSON.stringify.
Frequently Asked Questions
When should I use a Symbol instead of a string key?
What is the difference between Symbol() and Symbol.for()?
Conclusion
Symbol is JavaScript's seventh primitive type, designed for use cases where uniqueness and non-collision matter more than discoverability. They are the key mechanism behind JavaScript's iteration, string conversion, and species protocols. While everyday application code rarely creates symbols directly, understanding them explains how for...of loops, spread operators, and custom object behaviors actually work.
More in this topic
Using Decorators for Logging in JS Architecture
Learn how JavaScript decorators wrap class methods to add logging without touching the original code, including setup with Babel and how execution order works.
JavaScript While Loop Explained: A Complete Guide
A while loop repeats code as long as a condition stays true. Learn the syntax, see practical examples, and understand when a while loop is the right choice over a for loop.
JavaScript Loops Tutorial: for, while, do while
Loops let JavaScript repeat code without writing it twice. Learn the three core loop types, what each one does, and when to choose one over another.