Symbols
Understand what Symbol is, how it creates guaranteed-unique property keys, and the role well-known symbols like Symbol.iterator play in customizing built-in language behavior.
Symbol is JavaScript's seventh primitive type, introduced in ES6 specifically to solve one problem: creating property keys that are guaranteed never to collide with any other key, including ones added later by other code.
What a Symbol is
A Symbol is created by calling Symbol(), optionally with a description string for debugging purposes: const id = Symbol("user id"). Every call to Symbol() produces a brand-new, completely unique value — even Symbol("x") === Symbol("x") is false, because the description is just a label, not an identity.
This uniqueness is the entire point: a Symbol can never accidentally clash with a string key, a Symbol from a library, or a Symbol another part of your own code created — even if the description text is identical.
Symbols as unique object keys
Symbols are most often used as object property keys, as an alternative to string keys, specifically when you want a property that will not collide with normal string properties and will not show up during ordinary enumeration.
Properties keyed by a Symbol are skipped by for...in, Object.keys(), Object.values(), Object.entries(), and JSON.stringify(). They can only be discovered deliberately, via Object.getOwnPropertySymbols(). This makes Symbol keys useful for adding metadata to an object without interfering with its normal enumerable data.
Well-known symbols
Beyond symbols you create yourself, JavaScript defines a set of built-in 'well-known symbols' that let you hook into and customize core language behavior. The most commonly used is Symbol.iterator, covered in the previous lesson, which is what makes an object work with for...of and spread.
Other well-known symbols include Symbol.toPrimitive (customizes how an object converts to a primitive during math or string operations), Symbol.hasInstance (customizes instanceof behavior), and Symbol.toStringTag (customizes what Object.prototype.toString.call(obj) reports). These are advanced hooks most everyday code never needs to define, but understanding they exist explains how built-in objects like Array and Map customize their own behavior under the hood.
Quick reference
Symbol is never used with new — Symbol() (not new Symbol()) is how you create one; new Symbol() throws a TypeError. Use [ID]: value (a computed property key) to assign a symbol-keyed property inside an object literal. Symbol.for(key) checks a global registry first, returning the same symbol for the same key across your whole app; Symbol(key) never does this.
// creating symbols
const sym1 = Symbol();
const sym2 = Symbol("description for debugging");
console.log(sym1 === sym2); // false — always unique
// using a symbol as a property key
const ID = Symbol("id");
const user = {
name: "Alex",
[ID]: 12345 // computed property key syntax is required
};
console.log(user[ID]); // 12345
console.log(Object.keys(user)); // ['name'] — the symbol key is hidden
console.log(JSON.stringify(user));// '{"name":"Alex"}' — symbol key is skipped
// global symbol registry (shared, reusable symbols across a codebase)
const shared1 = Symbol.for("app.id");
const shared2 = Symbol.for("app.id");
console.log(shared1 === shared2); // true — Symbol.for reuses the same symbolCode Examples
Even with the exact same description string, two calls to Symbol() always produce two distinct values — the description is only for readability in debugging tools, not an identifier. Expected result: false symbol sym1 === sym2 is false because every Symbol() call creates a wholly new, unique value.
The record's internal ID is stored under a Symbol key so it stays out of normal enumeration (Object.keys, for...in, JSON.stringify), while still being fully accessible to any code that has a reference to the INTERNAL_ID symbol. Expected result: { name: 'Widget', [Symbol(internalId)]: '...' } [ 'name' ] '...' (a random base-36 string)
[INTERNAL_ID]: ... keys the property with the symbol instead of a plain string.
This shows two well-known symbols in one class: Symbol.iterator makes the deck spreadable/loopable, and Symbol.toStringTag customizes what Object.prototype.toString reports for instances of Deck, instead of the generic '[object Object]'. Expected result: [ 'A', 'K', 'Q' ] [object Deck] get [Symbol.toStringTag]() { return "Deck"; } is a well-known symbol hook that customizes a built-in language operation.
const sym1 = Symbol("token");
const sym2 = Symbol("token");
console.log(sym1 === sym2);
console.log(typeof sym1);const INTERNAL_ID = Symbol("internalId");
function createRecord(name) {
return {
name,
[INTERNAL_ID]: Math.random().toString(36).slice(2)
};
}
const record = createRecord("Widget");
console.log(record);
console.log(Object.keys(record));
console.log(record[INTERNAL_ID]);class Deck {
constructor(cards) {
this.cards = cards;
}
[Symbol.iterator]() {
let i = 0;
const cards = this.cards;
return {
next: () => i < cards.length
? { value: cards[i++], done: false }
: { value: undefined, done: true }
};
}
get [Symbol.toStringTag]() {
return "Deck";
}
}
const deck = new Deck(["A", "K", "Q"]);
console.log([...deck]);
console.log(Object.prototype.toString.call(deck));Important Rules
Symbol() always creates a brand-new, guaranteed-unique value, even when given the same description twice.
Symbol-keyed properties are excluded from for...in, Object.keys/values/entries, and JSON.stringify by default.
Use Symbol.for(key) instead of Symbol(key) when you specifically want the same symbol reused across different parts (or files) of an application.
Well-known symbols (Symbol.iterator, Symbol.toPrimitive, etc.) are the mechanism the language itself uses to let objects customize built-in behavior.
Where this shows up
Symbol.iterator is what makes Array, String, Map, and Set all compatible with for...of and spread. Library authors use unique symbols to attach hidden internal metadata to objects without any risk of colliding with a consumer's own property names — a guarantee plain string keys can never fully provide.
const id = new Symbol("id");
const id = Symbol("id");
Symbol is not a constructor in the usual sense — calling it with new throws a TypeError. Always call Symbol() directly, without new.
const user = { name: "Alex", ID: 1 }; // hoping ID stays private const ID = Symbol("id"); const user = { name: "Alex", [ID]: 1 };
A plain string key like "ID" is a completely ordinary, enumerable, discoverable property — it offers no protection from collisions or accidental enumeration. A Symbol key is what actually hides a property from normal enumeration.
[Multiple Choice] What does Object.keys() return for an object with one string-keyed property and one symbol-keyed property? A. Both keys B. Only the symbol key C. Only the string key D. An empty array
C. Only the string key — Object.keys() (along with for...in and JSON.stringify) only reports enumerable string keys — symbol-keyed properties are intentionally excluded and require Object.getOwnPropertySymbols() to discover.
[True / False] Symbol.for("app.id") called twice with the same string returns the exact same symbol both times. A. True B. False
A. True — True. Symbol.for uses a global symbol registry keyed by the string you pass — the first call creates the symbol and registers it, and every subsequent call with the same key returns that identical symbol.
Create a symbol called SECRET, add it as a hidden property on an object alongside a normal 'title' property, and demonstrate that Object.keys() only reports 'title' while the object still has the secret value accessible via the symbol.
const SECRET = Symbol("secret"); const obj = { title: "Public Title", [SECRET]: "hidden value" }; console.log(Object.keys(obj)); // ['title'] console.log(obj[SECRET]); // 'hidden value'
Use computed property syntax [SECRET]: value inside the object literal, then log Object.keys(obj) versus obj[SECRET].