Symbol Toolbox 0/6

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.

JavaScript ES202211 tools
01

Make one

Create a symbol, give it a label, and see why no two are ever equal.

Start hereSymbol()descriptionMust know

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.
Symbol(label?)

Makes a new, one of a kind value. No new keyword.

See the example
Gives a symbolMakes a new one
description

Reads the label you gave it.

See the example
Gives a string or undefinedOnly reads
toString()

Turns it into text like Symbol(id) on purpose.

See the example
Gives a stringOnly reads
make.jsJAVASCRIPT
const 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.

Terminal OUTPUT
$ node make.js
symbol
false
id
Symbol(id)
TypeError
02

Hidden keys

Use a symbol as an object key that ordinary code, loops and JSON will not see.

Keys[sym]getOwnPropertySymbols
ToolSees text keysSees symbol keys
Object.keysYesNo
for...inYesNo
JSON.stringifyYesNo
Object.getOwnPropertySymbolsNoYes
Reflect.ownKeysYesYes
keys.jsJAVASCRIPT
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.

Terminal OUTPUT
$ node keys.js
token-123
[ 'name' ]
{"name":"Asha"}
for in sees name
[ Symbol(secret) ]
[ 'name', Symbol(secret) ]
clash.jsJAVASCRIPT
// 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.

Terminal OUTPUT
$ node clash.js
A was here | B was here
[ 'name' ]
03

Shared symbols

Get the same symbol from anywhere in your app by name, using the global registry.

RegistrySymbol.forSymbol.keyFor
Symbol.for(name)

Finds the symbol with that name in a global list, or makes it the first time.

See the example
Gives a symbolMakes one if needed
Symbol.keyFor(sym)

Tells you the name a shared symbol was registered with.

See the example
Gives a string or undefinedOnly reads
shared.jsJAVASCRIPT
// 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.

Terminal OUTPUT
$ node shared.js
true
app.userId
undefined
04

Safe labels

Use symbols as status values that nothing else can accidentally match.

Patternsenumswitch

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.

enum.jsJAVASCRIPT
// 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.

Terminal OUTPUT
$ node enum.js
all done
unknown
05

Built in symbols

Teach your own objects to work with for...of, maths and text by using the symbols JavaScript already knows.

Well knownSymbol.iteratortoPrimitivetoStringTag
Symbol.iterator

Tells for...of and spread how to step through your object.

See the example
Makes it loopableChanges behaviour
Symbol.toPrimitive(hint)

Decides what your object becomes in maths or in text.

See the example
Gives a number or stringChanges behaviour
Symbol.toStringTag

Sets the name shown in [object Name].

See the example
Gives a stringChanges behaviour
Symbol.asyncIterator

Like iterator, but for for await...of.

See the example
Makes it async loopableChanges behaviour
iterator.jsJAVASCRIPT
// 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.

Terminal OUTPUT
$ node iterator.js
Mon
Tue
Wed
Mon -> Tue -> Wed
primitive.jsJAVASCRIPT
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".

Terminal OUTPUT
$ node primitive.js
500
Total: ₹450
[object Price]
06

All together

Build a Range you can loop and spread, with a hidden setting and a friendly name.

Putting it togetherSymbol()Symbol.iteratortoStringTag

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.

range.jsJAVASCRIPT
// 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.

Terminal OUTPUT
$ node range.js
[ 2, 4, 6, 8, 10 ]
[ 'start', 'end' ]
[object Range]
sum 1 to 4 = 10
TerminalBASH
# 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 toUseYou get
Make a one of a kind valueSymbol("label")a new symbol
Read its labelsym.descriptiona string
Use it as a key{ [sym]: value }a hidden key
Find symbol keysObject.getOwnPropertySymbols(obj)an array of symbols
See every keyReflect.ownKeys(obj)text and symbol keys
Share one by nameSymbol.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 nameget [Symbol.toStringTag]()[object Name]