Symbols

JavaScript · Lesson 26 of 61 8 min read Updated

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.

JavaScriptRun it yourself
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)}`);
Output
symbol
false
rollNo
Symbol(rollNo)
undefined
TypeError
Key: Symbol(rollNo)

Points to notice:

  • typeof gives "symbol", the seventh primitive type you met in Data Types.
  • Two symbols with the same description are still different.
  • .description reads the label; it is undefined if you gave none.
  • A symbol refuses to be silently turned into text, so "Key: " + id1 throws a TypeError. Convert it on purpose with String() or .toString(). This protects you from accidentally printing a symbol where you meant a string.
  • Symbol() is called without new. 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:

JavaScriptRun it yourself
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));
Output
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:

JavaScriptRun it yourself
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"));
Output
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.

JavaScriptRun it yourself
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));
Output
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.

SymbolUsed byLets your object...
Symbol.iteratorfor...of, spread ..., destructuring, Array.frombe looped over
Symbol.asyncIteratorfor await...ofbe looped over asynchronously
Symbol.toPrimitive+, *, ==, template literals, Number()decide how it becomes a number or string
Symbol.toStringTagObject.prototype.toStringchoose the name in [object Name]
Symbol.hasInstanceinstanceofcustomise 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:

JavaScriptRun it yourself
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);
Output
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).

JavaScriptRun it yourself
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));
Output
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.mySymbol reads the string key "mySymbol". Write obj[mySymbol].
  • Expecting the description to identify the symbol. Symbol("a") === Symbol("a") is false. Keep a reference to the symbol, or use Symbol.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 a TypeError; use String(sym) or sym.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...in and JSON.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.iterator and Symbol.toPrimitive let your objects work with for...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
JavaScript
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]);
Output
{"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?

2. Which of these lists an object's symbol keys?

3. How do you get the same symbol in two separate modules?

4. Which well-known symbol makes an object work with for...of and spread?

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