Symbols
Learn what Symbols are, why they exist, and how to use them in JavaScript.
Symbols
A Symbol is a primitive value that is guaranteed to be unique. Every time you create a Symbol, you get a value that is completely different from every other Symbol — even if they have the same description.
const id1 = Symbol("id");
const id2 = Symbol("id");
console.log(id1 === id2); // false — always unique
console.log(typeof id1); // "symbol"The string "id" is just a description — a label to help you identify the Symbol when debugging. It has no effect on uniqueness.
Why Symbols Exist
JavaScript objects use strings as property keys. The problem — if two pieces of code add a property with the same string key to the same object, they overwrite each other.
const user = { id: 1, name: "Ali" };
// Library A adds a property
user.meta = { source: "libraryA" };
// Library B also adds a property with the same name
user.meta = { source: "libraryB" }; // ❌ overwrites libraryA's propertySymbols solve this. Because every Symbol is unique, using a Symbol as a property key guarantees no collision — ever.
const metaA = Symbol("meta");
const metaB = Symbol("meta");
user[metaA] = { source: "libraryA" };
user[metaB] = { source: "libraryB" };
console.log(user[metaA]); // { source: "libraryA" }
console.log(user[metaB]); // { source: "libraryB" }
// Both coexist — no collisionCreating Symbols
// Basic symbol — description is optional but helpful
const sym = Symbol();
const namedSym = Symbol("description");
// Print a symbol — shows its description
console.log(namedSym.toString()); // "Symbol(description)"
console.log(namedSym.description); // "description"Symbols cannot be converted to strings automatically — this prevents accidental use:
const sym = Symbol("mySymbol");
console.log("Symbol is: " + sym); // ❌ TypeError — cannot convert to string
console.log(`Symbol is: ${sym}`); // ❌ TypeError — same
// Convert explicitly when you need to
console.log(sym.toString()); // "Symbol(mySymbol)"
console.log(String(sym)); // "Symbol(mySymbol)"Using Symbols as Object Keys
Use bracket notation to use a Symbol as a property key.
const ID = Symbol("id");
const CREATED_AT = Symbol("createdAt");
const user = {
name: "Ali",
[ID]: 1,
[CREATED_AT]: new Date()
};
console.log(user.name); // Ali
console.log(user[ID]); // 1
console.log(user[CREATED_AT]); // Date objectSymbol properties are hidden from most iteration
Symbol-keyed properties do not show up in for...in, Object.keys(), Object.values(), or Object.entries(). They are effectively invisible to normal enumeration.
const ID = Symbol("id");
const user = {
name: "Ali",
age: 22,
[ID]: 1
};
console.log(Object.keys(user)); // ["name", "age"] — no Symbol
console.log(Object.values(user)); // ["Ali", 22] — no Symbol
for (const key in user) {
console.log(key); // name, age — no Symbol
}To access Symbol properties specifically:
console.log(Object.getOwnPropertySymbols(user)); // [Symbol(id)]
// Get everything — including symbols
const allKeys = [
...Object.keys(user),
...Object.getOwnPropertySymbols(user)
];
console.log(allKeys); // ["name", "age", Symbol(id)]This hidden nature makes Symbols useful for metadata and internal implementation details that should not be exposed to regular code.
Global Symbol Registry — Symbol.for()
Sometimes you want to share a Symbol across different parts of your code — different files, different modules. Create a global Symbol with Symbol.for().
// Creates a symbol in the global registry — or returns existing one
const sym1 = Symbol.for("shared");
const sym2 = Symbol.for("shared");
console.log(sym1 === sym2); // true — same symbol from registryCompare with regular Symbol():
const local1 = Symbol("local");
const local2 = Symbol("local");
console.log(local1 === local2); // false — always different
const global1 = Symbol.for("global");
const global2 = Symbol.for("global");
console.log(global1 === global2); // true — same from registryLook up the key of a global symbol:
const sym = Symbol.for("auth");
console.log(Symbol.keyFor(sym)); // "auth"
const localSym = Symbol("local");
console.log(Symbol.keyFor(localSym)); // undefined — not in registryUse Symbol.for() when you need the same Symbol across modules. Use Symbol() when you want a unique Symbol local to one file.
Well-Known Symbols
JavaScript has a set of built-in Symbols called well-known symbols. They let you customize how objects behave with built-in JavaScript operations.
You already saw one — Symbol.iterator — which makes objects iterable with for...of.
Symbol.iterator — make an object iterable
class Range {
constructor(from, to) {
this.from = from;
this.to = to;
}
[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 };
}
};
}
}
const range = new Range(1, 5);
for (const num of range) {
console.log(num); // 1, 2, 3, 4, 5
}
console.log([...range]); // [1, 2, 3, 4, 5]Symbol.toPrimitive — control type conversion
Called when JavaScript tries to convert an object to a primitive value.
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}`;
return this.amount; // default
}
}
const price = new Money(1200, "PKR");
console.log(`Price: ${price}`); // Price: 1200 PKR — string hint
console.log(price * 2); // 2400 — number hint
console.log(price + 500); // 1700 — default hintSymbol.hasInstance — customize 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); // trueSymbol.toStringTag — customize Object.prototype.toString
class Collection {
get [Symbol.toStringTag]() {
return "Collection";
}
}
const col = new Collection();
console.log(Object.prototype.toString.call(col)); // "[object Collection]"A Practical Use — Non-Colliding Object Metadata
// A library that tracks metadata about DOM elements
const METADATA = Symbol("elementMetadata");
function attachMetadata(element, data) {
element[METADATA] = {
...data,
attachedAt: Date.now()
};
}
function getMetadata(element) {
return element[METADATA];
}
const btn = document.querySelector("button");
attachMetadata(btn, {
purpose: "submit-form",
trackingId: "btn-001"
});
console.log(getMetadata(btn));
// { purpose: "submit-form", trackingId: "btn-001", attachedAt: 1710000000000 }
// The metadata is invisible to regular code
console.log(Object.keys(btn)); // no METADATA key visibleThis is a common pattern in libraries — attach hidden metadata to objects without risking collision with user code or other libraries.
Symbols in Real Codebases
You will not create Symbols every day — but you will encounter them:
Symbol.iterator— whenever you make something iterableSymbol.for()— when sharing identifiers across modules- Well-known symbols — when customizing object behavior for built-in operations
- Library internals — many libraries use Symbols to store private state on user objects
Symbols are not used for security. Symbol properties can still be accessed with Object.getOwnPropertySymbols(). They are for uniqueness and collision prevention — not for hiding sensitive data.
Summary
- A Symbol is a guaranteed unique primitive value — no two Symbols are ever equal
- Created with
Symbol("description")— the description is just a label for debugging - Use Symbols as object property keys to avoid naming collisions
- Symbol properties are hidden from
for...in,Object.keys(), andObject.entries() - Access Symbol properties with
Object.getOwnPropertySymbols() Symbol.for("key")creates or retrieves a Symbol from the global registry — shared across modulesSymbol.keyFor(sym)retrieves the key of a global Symbol- Well-known symbols customize built-in JavaScript behavior —
Symbol.iterator,Symbol.toPrimitive,Symbol.hasInstance,Symbol.toStringTag - Use Symbols for non-colliding metadata, unique identifiers, and customizing language behavior