JavaScript basicsSymbols in plain wordsOne tool at a time
Symbol
Toolbox
A Symbol is a value that is guaranteed to be one of a kind. Nothing else can ever equal it. This guide shows how to make symbols, use them as hidden keys, share them, and use the built in ones that teach your objects new tricks, with a small example for each that you can run.
Key terms in plain words
Never met a symbol before, or code at all? Start here. Every word below shows up later on this page, and each one comes with an everyday comparison. Underlined words in the modules link back to these cards.
17 terms in 5 groups. Hover an underlined word anywhere on the page for a quick reminder, or click it to jump here.
What a symbol is
Symbols work like cloakroom tickets. Two tickets can have the same name written on them and still be different tickets.
- Symbol
- Think of it as a cloakroom ticket that no other ticket can ever match
- A kind of value in JavaScript that is one of a kind. Every call to
Symbol()makes a brand new one, and it equals nothing except itself. - For example
Symbol("id") === Symbol("id")isfalse. - One of a kind
- Think of it as a ticket number that is never handed out twice
- Most values equal any copy typed the same way:
"id" === "id"is true. A symbol only ever equals itself, so nothing can match it by accident. - Description (label)
- Think of it as the name scribbled on the ticket so staff can read it
- The text you pass to
Symbol("id"). It only helps you while debugging and has no effect on equality. Read it back with.description. - Turning a symbol into text
- Think of it as a ticket that refuses to be photocopied by accident
- A symbol won't quietly become text. Dropping one into a template literal (a backtick string with
${}in it) throws an error, so calltoString()or readdescriptionwhen you want words.
Hidden keys
Hanging something on a hook in the back room, where the usual staff never look.
- Object
- Think of it as a coat rack with a labelled hook for each coat
- A bundle of named values. Each name is a key and each value hangs under it.
- For example
{ name: "Asha", city: "Pune" } - Key
- Think of it as the label above one hook
- The name you use to store and read a value on an object. Keys are usually text, but a symbol can be a key too.
- For example In
user.name, the key isname. - Symbol key
- Think of it as a hook in the back room that only ticket holders know about
- A symbol used as a key, written in square brackets. Loops,
Object.keysand JSON skip it, so it stays out of the way. It isn't secret:Object.getOwnPropertySymbolsandReflect.ownKeysfind it on purpose. - For example
{ [secret]: "token-123" } - JSON
- Think of it as the printed list of coats, which leaves out the back room
- A plain text format for sending data between programs.
JSON.stringifyturns an object into that text, and it drops symbol keys completely.
Safe labels
Using tickets as status markers that a stray piece of text can never imitate.
- Enum
- Think of it as a fixed set of ticket colours the cloakroom accepts
- A named set of allowed values, such as a status of pending or paid. JavaScript has no enum keyword, so you build one from an object. Using symbols as the values means a typo or a matching string can never pass for one.
- For example
Status.PAIDmatches, the string"paid"does not. - Immutable
- Think of it as laminating the list so nobody can swap a ticket out
- Something that can't be changed after it's made.
Object.freezemakes an object's own values fixed, so the symbols it holds stay the ones you put there.
Built in symbols
Official tickets the building already knows, which make your own objects behave like built in ones.
- Well known symbol
- Think of it as an official stamp every machine in the building recognises
- Symbols that come with JavaScript, stored on
Symbolitself. Giving your object a method under one of them changes how the language treats it, in loops, in maths or when printed. - Iterator
- Think of it as an attendant who hands you the coats one at a time
- Something that gives out values one after another. A method named
[Symbol.iterator]makes your object loopable, sofor...ofand spread (...) work on it. - For example
for (const day of week)walks a customweekobject. - Generator
- Think of it as an attendant who pauses after each coat until you ask for the next
- A function written with a star,
function*, that hands out values withyieldand pauses in between. It's the shortest way to write an iterator. - toPrimitive and hint
- Think of it as a ticket that shows a number to the till and words to the customer
- A method under
Symbol.toPrimitivedecides what your object turns into. JavaScript passes a hint,"number","string"or"default", saying which it wants. - For example
+priceasks for a number,String(price)asks for text. - toStringTag
- Think of it as the name badge pinned to the rack
- Sets the name JavaScript shows when it describes your object, the
Namein[object Name]. - Class
- Think of it as the design every new coat rack is built from
- A blueprint for making objects that share the same setup and methods. The final example builds a
Rangeclass that uses every idea on this page. - For example
new Range(2, 10, 2)makes one range object.
Make one
Create a symbol, give it a label, and see why no two are ever equal.
Most values can be copied by typing them again: "id" === "id" is true. A Symbol is different. Every call to Symbol() makes a brand new value that equals nothing else, even another symbol with the same label.
Picture it: same label, different symbols
Symbol("id")vsSymbol("id")===falseThe label only helps you read it while debugging.Makes a new, one of a kind value. No new keyword.
See the exampleReads the label you gave it.
See the exampleTurns it into text like Symbol(id) on purpose.
See the exampleconst id1 = Symbol("id");
const id2 = Symbol("id"); // same label, still a different symbol
console.log(typeof id1);
console.log(id1 === id2); // every Symbol() call makes a one of a kind value
console.log(id1.description); // the label is only there to help you debug
console.log(id1.toString());
try { console.log(`${id1}`); } catch (err) { console.log(err.constructor.name); }
Watch for this: a symbol will not quietly turn into text. Putting it in a template literal throws, so call toString or read description.
$ node make.js symbol false id Symbol(id) TypeError
Hidden keys
Use a symbol as an object key that ordinary code, loops and JSON will not see.
| Tool | Sees text keys | Sees symbol keys |
|---|---|---|
Object.keys | Yes | No |
for...in | Yes | No |
JSON.stringify | Yes | No |
Object.getOwnPropertySymbols | No | Yes |
Reflect.ownKeys | Yes | Yes |
const secret = Symbol("secret");
const user = { name: "Asha", [secret]: "token-123" }; // square brackets for a symbol key
console.log(user[secret]); // read it with the same symbol
console.log(Object.keys(user)); // symbol keys are skipped
console.log(JSON.stringify(user)); // and left out of JSON
for (const k in user) console.log("for in sees", k);
console.log(Object.getOwnPropertySymbols(user)); // how to find them on purpose
console.log(Reflect.ownKeys(user)); // every key, text and symbol
Why it matters: symbol keys are not secret, since anyone can find them on purpose. They are just out of the way of normal code.
$ node keys.js token-123 [ 'name' ] {"name":"Asha"} for in sees name [ Symbol(secret) ] [ 'name', Symbol(secret) ]
// two different libraries both want to tag the same object
const libA = { tag: Symbol("tag") };
const libB = { tag: Symbol("tag") };
const item = { name: "invoice" };
item[libA.tag] = "A was here";
item[libB.tag] = "B was here"; // no clash, even with the same label
console.log(item[libA.tag], "|", item[libB.tag]);
console.log(Object.keys(item)); // the object looks untouched to everyone else
When to reach for it: adding your own data to an object you do not own, like a library object, without any chance of overwriting its keys.
$ node clash.js A was here | B was here [ 'name' ]
Shared symbols
Get the same symbol from anywhere in your app by name, using the global registry.
Finds the symbol with that name in a global list, or makes it the first time.
See the exampleTells you the name a shared symbol was registered with.
See the example// Symbol.for looks up a global list: same name, same symbol, anywhere
const a = Symbol.for("app.userId");
const b = Symbol.for("app.userId");
console.log(a === b);
console.log(Symbol.keyFor(a)); // the name it was registered with
console.log(Symbol.keyFor(Symbol("x"))); // a normal symbol is not in the list
Easy way to remember: Symbol() is always new. Symbol.for() is the same one every time you ask with the same name, even across files.
$ node shared.js true app.userId undefined
Safe labels
Use symbols as status values that nothing else can accidentally match.
Status values written as plain text, like "paid", can be matched by any string that happens to be the same. A symbol can only be matched by itself, so a typo or a lookalike value can never slip through.
// symbols make safe labels: nothing else can accidentally equal them
const Status = Object.freeze({
PENDING: Symbol("pending"),
PAID: Symbol("paid"),
});
function label(status) {
switch (status) {
case Status.PENDING: return "waiting for payment";
case Status.PAID: return "all done";
default: return "unknown";
}
}
console.log(label(Status.PAID));
console.log(label("paid")); // plain text cannot pretend to be the symbol
Rule of thumb: freeze the object that holds the symbols so no one can swap one out later.
$ node enum.js all done unknown
Built in symbols
Teach your own objects to work with for...of, maths and text by using the symbols JavaScript already knows.
Tells for...of and spread how to step through your object.
See the exampleDecides what your object becomes in maths or in text.
See the exampleSets the name shown in [object Name].
See the exampleLike iterator, but for for await...of.
See the example// Symbol.iterator tells for...of and spread how to walk your object
const week = {
days: ["Mon", "Tue", "Wed"],
*[Symbol.iterator]() {
for (const d of this.days) yield d;
},
};
for (const day of week) console.log(day);
console.log([...week].join(" -> "));
The pattern: a generator method with a star, named with [Symbol.iterator], is the shortest way to make any object loopable.
$ node iterator.js Mon Tue Wed Mon -> Tue -> Wed
const price = {
amount: 450,
// decides what this object becomes in maths or in text
[Symbol.toPrimitive](hint) {
return hint === "number" ? this.amount : `₹${this.amount}`;
},
get [Symbol.toStringTag]() { return "Price"; }, // the name in toString
};
console.log(+price + 50); // number hint
console.log(`Total: ${price}`); // string hint
console.log(Object.prototype.toString.call(price));
Why it matters: the hint tells you what JavaScript wants. Return a number for "number" and text for "string" or "default".
$ node primitive.js 500 Total: ₹450 [object Price]
All together
Build a Range you can loop and spread, with a hidden setting and a friendly name.
This class brings every idea together. A private style symbol key holds the step size out of the way, Symbol.iterator makes it work with for...of and spread, and Symbol.toStringTag gives it a readable name.
// a Range you can loop over, with a hidden step setting
const STEP = Symbol("step");
class Range {
constructor(start, end, step = 1) {
this.start = start;
this.end = end;
this[STEP] = step; // a symbol key keeps it out of the way
}
*[Symbol.iterator]() { // makes for...of and spread work
for (let n = this.start; n <= this.end; n += this[STEP]) yield n;
}
get [Symbol.toStringTag]() { return "Range"; }
}
const evens = new Range(2, 10, 2);
console.log([...evens]);
console.log(Object.keys(evens)); // step does not show up as a normal key
console.log(String(evens));
let total = 0;
for (const n of new Range(1, 4)) total += n;
console.log("sum 1 to 4 =", total);
What each part uses: a hidden key from module 02, and Symbol.iterator plus Symbol.toStringTag from module 05.
$ node range.js [ 2, 4, 6, 8, 10 ] [ 'start', 'end' ] [object Range] sum 1 to 4 = 10
# save any example as a file and run it with Node 22 $ node range.js # or paste it into your browser console with F12
Which one do I need?
Start from what you want to do, then read across to the tool and what it gives back.
| I want to | Use | You get |
|---|---|---|
| Make a one of a kind value | Symbol("label") | a new symbol |
| Read its label | sym.description | a string |
| Use it as a key | { [sym]: value } | a hidden key |
| Find symbol keys | Object.getOwnPropertySymbols(obj) | an array of symbols |
| See every key | Reflect.ownKeys(obj) | text and symbol keys |
| Share one by name | Symbol.for("name") | the same symbol each time |
| Make an object loopable | *[Symbol.iterator]() {} | for...of and spread work |
| Control maths and text | [Symbol.toPrimitive](hint) | your own conversion |
| Set the display name | get [Symbol.toStringTag]() | [object Name] |