Symbols
Every string is just text: two pieces of code that both pick the key "id" for an object will overwrite each other's data. A symbol is a value that is guaranteed to be unique. No other symbol is ever equal to it, so a symbol used as an object key can never clash with anyone else's key.
In this lesson you will create symbols, use them as object keys, share symbols with Symbol.for(), and use the built-in "well-known" symbols that let your own objects work with for...of, + and template literals. Symbols are an intermediate topic: you will mostly meet them inside libraries and in classes that plug into the language.
Creating a symbol
Call Symbol() with an optional description, a label that helps you when debugging. The description does not make the symbol unique or equal to anything; every call creates a brand-new symbol.
const id1 = Symbol("rollNo");
const id2 = Symbol("rollNo");
console.log(typeof id1);
console.log(id1 === id2);
console.log(id1.description);
console.log(id1.toString());
console.log(Symbol().description);
try {
console.log("Key: " + id1);
} catch (error) {
console.log(error.name);
}
console.log(`Key: ${String(id1)}`);symbol false rollNo Symbol(rollNo) undefined TypeError Key: Symbol(rollNo)
Points to notice:
typeofgives"symbol", the seventh primitive type you met in Data Types.- Two symbols with the same description are still different.
.descriptionreads the label; it isundefinedif you gave none.- A symbol refuses to be silently turned into text, so
"Key: " + id1throws aTypeError. Convert it on purpose withString()or.toString(). This protects you from accidentally printing a symbol where you meant a string. Symbol()is called withoutnew.new Symbol()throws an error.
Symbols as object keys
Most of the time, a symbol is used as a property key. Put it in square brackets, both when you create the object and when you read the property:
const internalId = Symbol("internalId");
const student = {
name: "Ananya",
city: "Lucknow",
[internalId]: 98231,
};
console.log(student[internalId]);
console.log(Object.keys(student));
console.log(JSON.stringify(student));
for (const key in student) console.log("for...in sees:", key);
console.log(Object.getOwnPropertySymbols(student));
console.log(Reflect.ownKeys(student));98231
[ 'name', 'city' ]
{"name":"Ananya","city":"Lucknow"}
for...in sees: name
for...in sees: city
[ Symbol(internalId) ]
[ 'name', 'city', Symbol(internalId) ]Symbol keys are skipped by Object.keys(), for...in and JSON.stringify(). That makes them useful for metadata: extra information a library attaches to your objects without showing up when you loop over them or save them as JSON.
But symbol keys are not secret. Object.getOwnPropertySymbols() and Reflect.ownKeys() list them, and anyone with the symbol can read the value. For truly private data inside a class, use private fields such as #marks (see Classes).
A common mistake is writing student.internalId. The dot form always uses the string "internalId", not your symbol, so it gives undefined. Symbol keys always need square brackets.
Symbols as named constants
Symbols also make good constants for a fixed set of states, because no string or number can be mistaken for them:
const Status = Object.freeze({
PENDING: Symbol("pending"),
SHIPPED: Symbol("shipped"),
DELIVERED: Symbol("delivered"),
});
function label(status) {
switch (status) {
case Status.PENDING: return "Order received";
case Status.SHIPPED: return "On the way";
case Status.DELIVERED: return "Delivered";
default: return "Unknown status";
}
}
console.log(label(Status.SHIPPED));
console.log(label("shipped"));On the way Unknown status
The plain string "shipped" does not match, because only the real symbol is equal to Status.SHIPPED. The downside is that symbols cannot be saved in JSON or localStorage, so use strings for values that must be stored or sent to a server.
Shared symbols with Symbol.for
Sometimes two separate pieces of code, such as two modules or a page and an iframe, need the same symbol. Symbol.for(key) looks the key up in a global symbol registry: the first call creates the symbol, and every later call with the same key returns that same symbol. Symbol.keyFor() does the reverse.
const a = Symbol.for("app.theme");
const b = Symbol.for("app.theme");
const c = Symbol("app.theme");
console.log(a === b);
console.log(a === c);
console.log(Symbol.keyFor(a));
console.log(Symbol.keyFor(c));true false app.theme undefined
c was made with plain Symbol(), so it is not in the registry. Because the registry is shared by all code on the page, use long, specific keys such as "myshop.cart.version" to avoid collisions.
Well-known symbols
JavaScript itself uses a set of built-in symbols, stored as properties of Symbol, to ask objects how they want to behave. They are called well-known symbols. By adding a method with one of these keys, your object plugs into a language feature.
| Symbol | Used by | Lets your object... |
|---|---|---|
Symbol.iterator | for...of, spread ..., destructuring, Array.from | be looped over |
Symbol.asyncIterator | for await...of | be looped over asynchronously |
Symbol.toPrimitive | +, *, ==, template literals, Number() | decide how it becomes a number or string |
Symbol.toStringTag | Object.prototype.toString | choose the name in [object Name] |
Symbol.hasInstance | instanceof | customise instanceof checks |
Symbol.iterator
You built iterable classes in Iterators, Generators & Iterator Helpers. Here is a quick reminder of how much one method gives you:
class Batch {
constructor(name, students) {
this.name = name;
this.students = students;
}
*[Symbol.iterator]() {
yield* this.students;
}
}
const morning = new Batch("Morning batch", ["Aarav", "Priya", "Farhan"]);
for (const student of morning) console.log(student);
console.log([...morning]);
const [first, ...others] = morning;
console.log(first, others);Aarav Priya Farhan [ 'Aarav', 'Priya', 'Farhan' ] Aarav [ 'Priya', 'Farhan' ]
Symbol.toPrimitive
In Type Conversion & Coercion you saw objects become primitives through valueOf() and toString(). A Symbol.toPrimitive method replaces both. JavaScript passes a hint saying what it wants: "number", "string" or "default" (used by + and ==, where either would do).
class Money {
constructor(paise) {
this.paise = paise;
}
[Symbol.toPrimitive](hint) {
console.log("hint:", hint);
if (hint === "string") return `₹${(this.paise / 100).toFixed(2)}`;
return this.paise / 100; // "number" and "default"
}
get [Symbol.toStringTag]() {
return "Money";
}
}
const fee = new Money(129950);
console.log(`Fee: ${fee}`);
console.log(fee * 2);
console.log(fee + 50);
console.log(Object.prototype.toString.call(fee));hint: string Fee: ₹1299.50 hint: number 2599 hint: default 1349.5 [object Money]
The template literal asked for a string, * asked for a number, and + sent "default". Storing money as whole paise avoids decimal rounding problems (see Numbers & Math). Use this feature sparingly: an object that quietly turns into a number can confuse readers, and an explicit method such as fee.toRupees() is often clearer.
Common mistakes
- Using dot notation with a symbol.
obj.mySymbolreads the string key"mySymbol". Writeobj[mySymbol]. - Expecting the description to identify the symbol.
Symbol("a") === Symbol("a")isfalse. Keep a reference to the symbol, or useSymbol.for()when it must be shared. - Thinking symbol keys are private. They are only hidden from normal loops and JSON.
- Joining a symbol with
+. It throws aTypeError; useString(sym)orsym.description. - Storing symbols. JSON and Web Storage cannot hold them; use strings for data you save.
Key points
Symbol(description)creates a unique primitive value; the description is only a label.- Symbol keys are written in square brackets and are skipped by
Object.keys(),for...inandJSON.stringify(). - Symbol keys are not private:
Object.getOwnPropertySymbols()lists them. Symbol.for(key)returns a shared symbol from the global registry;Symbol.keyFor()reads its key.- Well-known symbols such as
Symbol.iteratorandSymbol.toPrimitivelet your objects work withfor...of, spread, operators and template literals.
Exercise
Create a symbol lastSeen and add it as a key to two user objects (Kabir and Meera), with a time string as the value. Show that JSON.stringify() of each user does not include it, but that you can still read it with square brackets. Then give a Playlist class a Symbol.iterator method so that [...playlist] returns its song titles.
Show solution
const lastSeen = Symbol("lastSeen");
const users = [
{ name: "Kabir", city: "Rohtak", [lastSeen]: "10:42" },
{ name: "Meera", city: "Kochi", [lastSeen]: "11:05" },
];
for (const user of users) {
console.log(JSON.stringify(user), user[lastSeen]);
}
class Playlist {
constructor(...songs) {
this.songs = songs;
}
*[Symbol.iterator]() {
for (const song of this.songs) yield song.title;
}
}
const roadTrip = new Playlist({ title: "Kesariya" }, { title: "Ilahi" });
console.log([...roadTrip]);{"name":"Kabir","city":"Rohtak"} 10:42
{"name":"Meera","city":"Kochi"} 11:05
[ 'Kesariya', 'Ilahi' ]JSON skips symbol keys, but the values are still on the objects. The generator method yields only each song's title, so spreading the playlist gives an array of titles.
Quick Quiz
1. What does Symbol("x") === Symbol("x") return?
Every call to Symbol() creates a new, unique symbol; the description is only a label.
2. Which of these lists an object's symbol keys?
Symbol keys are skipped by normal key loops and JSON, but Object.getOwnPropertySymbols() and Reflect.ownKeys() include them.
3. How do you get the same symbol in two separate modules?
Symbol.for() uses the global symbol registry, so the same key returns the same symbol everywhere.
4. Which well-known symbol makes an object work with for...of and spread?
for...of, spread and destructuring all call the method stored under Symbol.iterator.
Learn JavaScript with a mentor
Free lessons get you started. Our Full Stack Web Development training adds real projects and code reviews from working developers.
- Project-based learning
- Mentor code reviews
- Free counselling call
