JavaScript Symbols: Unique Keys and Symbol.iterator Explained
Learn JavaScript Symbols: create unique values, use them as hidden object keys, share them with Symbol.for, and build iterables with Symbol.iterator.
What you will learn
JavaScript has seven primitive types: string, number, boolean, null, undefined, bigint and one you may not have used yet: Symbol. A symbol is a value that is guaranteed to be unique. That sounds strange, but it solves real problems: object keys that never clash, constants that cannot be confused with each other, and the special “hooks” that make features like for...of, spread and String(obj) work. In this lesson you will learn how to create symbols, how to use them as property keys, how the global symbol registry works, and the most important built-in symbol, Symbol.iterator, which lets you build your own iterable objects.
What is a Symbol?
A symbol is created by calling Symbol(). Do not use new.
const id = Symbol();
console.log(typeof id); // "symbol"
console.log(id); // Symbol()
You can add an optional description (a label). It is only for debugging and does not change the symbol.
const userId = Symbol("userId");
console.log(userId); // Symbol(userId)
console.log(userId.description); // "userId"
console.log(userId.toString()); // "Symbol(userId)"
Every symbol is unique
Even two symbols with the same description are different:
const a = Symbol("id");
const b = Symbol("id");
console.log(a === b); // false
console.log(a === a); // true
This uniqueness is the whole point of symbols.
new Symbol() does not work
// const s = new Symbol("x"); // TypeError: Symbol is not a constructor
const s = Symbol("x"); // correct
Symbols and type conversion
Symbols are strict about conversion, to stop you from mixing them up with text by mistake.
const s = Symbol("tag");
console.log(String(s)); // "Symbol(tag)" (explicit conversion works)
console.log(s.toString()); // "Symbol(tag)"
console.log(Boolean(s)); // true
// console.log(s + ""); // TypeError: Cannot convert a Symbol value to a string
// console.log(`${s}`); // TypeError
// console.log(s + 1); // TypeError: Cannot convert a Symbol value to a number
So use String(s) or s.description when you want to show a symbol.
Symbols as object keys
Normally, object keys are text. A symbol can also be a key. Put it in square brackets (a computed key):
const id = Symbol("id");
const user = {
name: "Riya",
[id]: 101
};
console.log(user[id]); // 101
console.log(user.name); // "Riya"
To read it, you need the same symbol. You cannot use dot notation:
console.log(user.id); // undefined (that is a text key "id", not our symbol)
console.log(user["id"]); // undefined
console.log(user[id]); // 101
Adding a symbol key later works the same way:
const note = Symbol("note");
const task = { title: "Learn JS" };
task[note] = "Practise every day";
console.log(task[note]); // "Practise every day"
Symbol keys are hidden from most loops
Symbol-keyed properties are skipped by the usual ways of listing an object:
const secret = Symbol("secret");
const user = {
name: "Riya",
age: 22,
[secret]: "hidden"
};
console.log(Object.keys(user)); // ["name", "age"]
console.log(Object.values(user)); // ["Riya", 22]
console.log(Object.entries(user)); // [["name", "Riya"], ["age", 22]]
console.log(JSON.stringify(user)); // {"name":"Riya","age":22}
for (const key in user) {
console.log(key); // "name", "age" (no secret)
}
This makes symbols perfect for adding extra information to an object without disturbing code that loops over it or sends it to a server.
How to find symbol keys
They are not truly private. You can find them with special methods:
const secret = Symbol("secret");
const user = { name: "Riya", [secret]: "hidden" };
console.log(Object.getOwnPropertySymbols(user)); // [Symbol(secret)]
console.log(Reflect.ownKeys(user)); // ["name", Symbol(secret)] (all keys, text and symbols)
Important: symbols give you hiding, not security. Anyone who has access to the object can find its symbol keys. For real privacy in classes, use
#privatefields (OOP section).
Spread and Object.assign do copy symbol keys that are enumerable:
const tag = Symbol("tag");
const original = { a: 1, [tag]: "x" };
const copy = { ...original };
console.log(copy[tag]); // "x"
Why symbols exist: avoiding name clashes
Imagine you use a library that gives objects an id property, and you also want your own id. They would overwrite each other. With a symbol, your key can never collide with any other key:
// Code from a library or another developer
const product = { id: "lib-55", name: "Pen" };
// Your own hidden data
const myId = Symbol("myId");
product[myId] = "my-001";
console.log(product.id); // "lib-55" (untouched)
console.log(product[myId]); // "my-001"
The global symbol registry: Symbol.for
Normal symbols are always unique. Sometimes you want the same symbol in different files. Symbol.for(key) looks in a global registry: if a symbol with that key exists, it returns it, otherwise it creates one.
const s1 = Symbol.for("app.user");
const s2 = Symbol.for("app.user");
console.log(s1 === s2); // true (same symbol from the registry)
console.log(Symbol("app.user") === Symbol.for("app.user")); // false (a normal symbol is never in the registry)
Symbol.keyFor(symbol) goes backwards: it gives the registry key.
const shared = Symbol.for("app.user");
console.log(Symbol.keyFor(shared)); // "app.user"
console.log(Symbol.keyFor(Symbol("normal"))); // undefined (not in the registry)
Tip: use a clear prefix like
"myapp.something"for registry keys, so they do not clash with other code.
Symbols as constants (enum-like values)
Symbols are great as constants, because each one is unique and can never be confused with a text or a number:
const Status = Object.freeze({
PENDING: Symbol("pending"),
ACTIVE: Symbol("active"),
DONE: Symbol("done")
});
function describe(status) {
switch (status) {
case Status.PENDING: return "Waiting to start";
case Status.ACTIVE: return "In progress";
case Status.DONE: return "Finished";
default: return "Unknown status";
}
}
console.log(describe(Status.ACTIVE)); // "In progress"
console.log(describe("active")); // "Unknown status" (a text cannot pretend to be a status)
The small downside: symbols do not survive JSON.stringify or localStorage. For data you save or send, plain text constants are usually better.
Well-known symbols
JavaScript has built-in symbols that act as hooks. If your object has a method under one of these keys, JavaScript uses it in a special situation. They are properties of Symbol.
| Symbol | Where JavaScript uses it |
|---|---|
Symbol.iterator |
for...of, spread [...x], destructuring, Array.from, new Map(x), new Set(x) |
Symbol.asyncIterator |
for await...of |
Symbol.toPrimitive |
Converting an object to a primitive (+obj, ${obj}, obj + 1) |
Symbol.toStringTag |
The label in Object.prototype.toString.call(obj) |
Symbol.hasInstance |
The instanceof operator |
Symbol.species, Symbol.isConcatSpreadable, Symbol.match and others |
Deeper customisation |
The most important one for you right now is Symbol.iterator.
Symbol.iterator and iterables
What is an iterable?
An iterable is any object that for...of can loop over. Arrays, strings, Map and Set are iterables. What do they have in common? They all have a method stored under the key Symbol.iterator.
const array = [10, 20, 30];
console.log(typeof array[Symbol.iterator]); // "function"
const text = "Hi";
console.log(typeof text[Symbol.iterator]); // "function"
const plain = { a: 1 };
console.log(typeof plain[Symbol.iterator]); // "undefined" (plain objects are NOT iterable)
That is why for (const x of { a: 1 }) fails with TypeError: ... is not iterable.
How iteration works behind the scenes
When you write for (const x of something), JavaScript does this:
- It calls
something[Symbol.iterator](). This returns an iterator. - It calls the iterator’s
next()method again and again. - Each
next()returns an object{ value, done }. - When
doneistrue, the loop ends.
You can do this by hand:
const array = ["a", "b"];
const iterator = array[Symbol.iterator]();
console.log(iterator.next()); // { value: "a", done: false }
console.log(iterator.next()); // { value: "b", done: false }
console.log(iterator.next()); // { value: undefined, done: true }
Make your own iterable
To make your object work with for...of, give it a [Symbol.iterator]() method that returns an object with a next() method.
const countdown = {
from: 5,
[Symbol.iterator]() {
let current = this.from;
return {
next() {
if (current > 0) {
return { value: current--, done: false };
}
return { value: undefined, done: true };
}
};
}
};
for (const n of countdown) {
console.log(n); // 5, 4, 3, 2, 1
}
Once your object is iterable, all the features that use iterables work with it:
console.log([...countdown]); // [5, 4, 3, 2, 1]
console.log(Array.from(countdown)); // [5, 4, 3, 2, 1]
console.log(Math.max(...countdown)); // 5
const [first, second] = countdown;
console.log(first, second); // 5 4
console.log(new Set(countdown).size); // 5
A class-based iterable: a range
class Range {
constructor(start, end, step = 1) {
this.start = start;
this.end = end;
this.step = step;
}
[Symbol.iterator]() {
let value = this.start;
const { end, step } = this;
return {
next() {
if (value <= end) {
const result = { value, done: false };
value += step;
return result;
}
return { value: undefined, done: true };
}
};
}
}
console.log([...new Range(1, 5)]); // [1, 2, 3, 4, 5]
console.log([...new Range(0, 10, 5)]); // [0, 5, 10]
A shortcut with generators (preview)
Writing the next() method by hand is long. A generator function (function*) with yield does the same job in a few lines. You will learn generators later; here is a small preview:
const countdown = {
from: 3,
*[Symbol.iterator]() {
for (let n = this.from; n > 0; n--) {
yield n;
}
}
};
console.log([...countdown]); // [3, 2, 1]
Iterables can be infinite and lazy
An iterator produces one value at a time, only when asked. So it can describe a sequence that never ends, and you take only what you need:
const naturals = {
[Symbol.iterator]() {
let n = 1;
return { next: () => ({ value: n++, done: false }) };
}
};
for (const n of naturals) {
if (n > 5) break; // stop yourself, or the loop never ends
console.log(n); // 1, 2, 3, 4, 5
}
Symbol.toPrimitive: control conversion
If an object has a [Symbol.toPrimitive](hint) method, JavaScript uses it when it must turn the object into a number or text. The hint is "string", "number" or "default".
const temperature = {
celsius: 25,
[Symbol.toPrimitive](hint) {
if (hint === "string") return `${this.celsius}°C`;
return this.celsius; // "number" and "default"
}
};
console.log(`${temperature}`); // "25°C" (hint: string)
console.log(+temperature); // 25 (hint: number)
console.log(temperature + 5); // 30 (hint: default)
console.log(temperature > 20); // true
Symbol.toStringTag: a friendly label
class Money {
get [Symbol.toStringTag]() {
return "Money";
}
}
const price = new Money();
console.log(Object.prototype.toString.call(price)); // "[object Money]"
console.log(String(price)); // "[object Money]"
Without it, the label would be [object Object].
Symbol.hasInstance: customise instanceof
class Even {
static [Symbol.hasInstance](value) {
return Number.isInteger(value) && value % 2 === 0;
}
}
console.log(4 instanceof Even); // true
console.log(7 instanceof Even); // false
Real-life use cases
1. Attach hidden metadata to an object
const meta = Symbol("meta");
function track(item) {
item[meta] = { createdAt: 1000, source: "import" };
return item;
}
const product = track({ name: "Pen", price: 10 });
console.log(Object.keys(product)); // ["name", "price"]
console.log(JSON.stringify(product)); // {"name":"Pen","price":10} (metadata not sent)
console.log(product[meta].source); // "import"
2. A safe way to extend objects you do not own
const visited = Symbol("visited");
function markVisited(obj) {
obj[visited] = true;
}
const page = { url: "/home" };
markVisited(page);
console.log(page[visited]); // true
console.log(page); // { url: "/home", [Symbol(visited)]: true }
3. A playlist you can loop over
class Playlist {
#songs = [];
add(song) {
this.#songs.push(song);
return this;
}
[Symbol.iterator]() {
let index = 0;
const songs = this.#songs;
return {
next: () =>
index < songs.length
? { value: songs[index++], done: false }
: { value: undefined, done: true }
};
}
}
const playlist = new Playlist().add("Song A").add("Song B").add("Song C");
for (const song of playlist) {
console.log(song);
}
console.log([...playlist].length); // 3
The songs stay private, and the outside code still gets a natural way to loop.
4. Making a plain object iterable over its entries
const scores = {
Riya: 90,
Karan: 75,
*[Symbol.iterator]() {
for (const [name, score] of Object.entries(this)) {
yield `${name}: ${score}`;
}
}
};
console.log([...scores]); // ["Riya: 90", "Karan: 75"]
(Symbol-keyed methods are skipped by Object.entries, so the iterator method itself does not appear in the list.)
5. Unique event types that cannot be mistyped
const EVENTS = Object.freeze({
LOGIN: Symbol("login"),
LOGOUT: Symbol("logout")
});
const handlers = new Map();
function on(event, fn) {
handlers.set(event, fn);
}
function emit(event, data) {
handlers.get(event)?.(data);
}
on(EVENTS.LOGIN, (user) => console.log(`${user} logged in`));
emit(EVENTS.LOGIN, "Riya"); // "Riya logged in"
emit("login", "Riya"); // nothing happens: a text is not the event symbol
6. Shared symbol across files
// file-a.js
const key = Symbol.for("mycodenest.theme");
const store = { [key]: "dark" };
// file-b.js (a different file can get the very same symbol)
const sameKey = Symbol.for("mycodenest.theme");
console.log(store[sameKey]); // "dark"
7. Fibonacci numbers, one at a time
const fibonacci = {
[Symbol.iterator]() {
let [a, b] = [0, 1];
return {
next() {
const value = a;
[a, b] = [b, a + b];
return { value, done: false };
}
};
}
};
const firstEight = [];
for (const n of fibonacci) {
if (firstEight.length === 8) break;
firstEight.push(n);
}
console.log(firstEight); // [0, 1, 1, 2, 3, 5, 8, 13]
Common mistakes
- Using
new Symbol(). Symbols are created by callingSymbol()withoutnew. - Reading a symbol key with dot notation like
obj.id. Useobj[id]with the same symbol. - Creating a new
Symbol("x")each time and expecting it to equal the old one. Every call makes a different symbol. Store it in a constant, or useSymbol.for. - Joining a symbol with text (
"id: " + symor a template literal). It throws aTypeError. Usesym.descriptionorString(sym). - Expecting symbols to be private or secure. They are only hidden from normal loops.
Object.getOwnPropertySymbolsfinds them. - Expecting symbol keys in
JSON.stringify,Object.keysorfor...in. They are skipped on purpose. - Saving symbols in
localStorageor sending them to an API. They do not survive JSON. Use text for stored data. - Forgetting that plain objects are not iterable. Add
[Symbol.iterator], or useObject.entries(obj)withfor...of. - Returning a wrong shape from
next(). It must return an object withvalueanddone. - Creating an infinite iterable and looping without
break. The loop never ends and the page freezes. - Using
Symbol.forwith a very common key like"id". Anyone can get the same symbol. Use a clear prefix.
Practice
- Create two symbols with the same description and show that they are not equal. Print their
typeofanddescription. - Create an object with a normal
nameproperty and a symbol property. PrintObject.keys,JSON.stringify, andObject.getOwnPropertySymbolsfor it. - Show what happens when you try
"Symbol: " + Symbol("x"), and fix it with.description. - Use
Symbol.for("app.theme")in two places and prove that you get the same symbol. UseSymbol.keyForto read its key. - Create a
Statusobject with three symbol constants, and write a function that returns a message for each status usingswitch. - Loop over a string and an array with
for...of, then call[Symbol.iterator]()andnext()by hand on the array. - Make an object
weekdayswith a list of days and a[Symbol.iterator]so that[...weekdays]returns the days. - Write a
Rangeclass (start, end, step) that works withfor...ofand spread. - Give an object a
[Symbol.toPrimitive]method so that${obj}gives text, andobj + 1gives a number. - Challenge: write a function
take(iterable, n)that returns the firstnvalues of any iterable (even an infinite one) as an array.
Recap
- A Symbol is a primitive type whose values are unique. Create one with
Symbol("description"), withoutnew. - Two symbols are never equal, even with the same description. The description (
sym.description) is just a label for debugging. - Symbols do not convert to text automatically. Use
String(sym)orsym.description. - Use a symbol as an object key with
[sym]. Symbol keys are skipped byObject.keys,for...inandJSON.stringify, but they can be found withObject.getOwnPropertySymbolsandReflect.ownKeys. They hide data, they do not secure it. - Symbols prevent key collisions, and they work well as unique constants.
Symbol.for(key)returns a shared symbol from the global registry, andSymbol.keyFor(sym)reads its key.- Well-known symbols such as
Symbol.iterator,Symbol.toPrimitive,Symbol.toStringTagandSymbol.hasInstancelet your objects plug into built-in JavaScript behaviour. - An object with a
[Symbol.iterator]()method that returns an object withnext()(giving{ value, done }) is iterable, so it works withfor...of, spread, destructuring,Array.from,MapandSet. - Next you will start the DOM and the Browser section, where JavaScript finally changes real web pages. First stop: the DOM introduction.