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.
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] |