Command Palette

Search for a command to run...

Hectal
PHASE 14Advanced ES2015+ ~19 min· topic 5 of 9

Topic 14.5

Symbols and Well-Known Symbols

In one line

A symbol is a unique, primitive value used as a property key that can never clash with another key; the language's own well-known symbols, such as Symbol.iterator and Symbol.toPrimitive, are hooks that let your objects change how built-in operations treat them.

Think of it like this

A school locker with your own key. Two students can both write "Locker" on their tags, but each key opens only one lock, and no other key ever will. A symbol is such a key for an object property: you can give it a label like "id", but two symbols with the same label are still different keys, so nobody else's property can accidentally open (or overwrite) yours.

Words you'll meet

New words in this topic, in plain English. Come back here whenever one feels fuzzy.

Symbol
A unique primitive value, mostly used as a property key that can't clash with any other key.
Description
The label given to Symbol("label"), only for debugging. It doesn't make the symbol unique or equal to another.
Computed key
A property name worked out from an expression in square brackets, like { [id]: 42 }.
Global symbol registry
A shared table where Symbol.for("name") stores and finds symbols by a string key.
Well-known symbol
A built-in symbol such as Symbol.iterator that the language looks for to decide how to treat an object.
Hint
The word ("number", "string" or "default") the engine passes to Symbol.toPrimitive to say what kind of value it wants.
Primitive
A simple value that isn't an object: string, number, bigint, boolean, undefined, null or symbol.

Step by step

01Create two symbols with the same label

The label is just a name tag. The values are still different.

unique.jswhole filejavascript
const a = Symbol("id");
const b = Symbol("id");
console.log(a === b); // false
console.log(a.description); // "id"

02Use one as a property key

Imagine your code adds an id to objects from a library. A string key "id" could overwrite the library's own id. A symbol key can't, and it also stays out of Object.keys and JSON.

tag.jswhole filejavascript
const trackingId = Symbol("trackingId");
const order = { id: 7, total: 250 };
order[trackingId] = "t-91";
console.log(order.id, order[trackingId]); // 7 t-91
console.log(JSON.stringify(order)); // {"id":7,"total":250}

03Share a symbol with Symbol.for

Two separate files that both call Symbol.for("shop.cart") get the very same symbol, because it comes from the global registry.

04Hook into the language

Define a well-known symbol method on your class, and built-in operations call it. The engine checks for these hooks at specific steps in the spec.

Hook into the languagediagram
Rendering diagram…

05Use symbols as unique constants

Status codes built from symbols can't be faked with a string or confused with another constant that has the same text.

Try it yourself

  1. 1

    Make it a registry symbol

    In the third example, change both Symbol("active") calls to Symbol.for("active"). Predict what Mallory gets now, and explain why that weakens the constant.

  2. 2

    Remove the toPrimitive method

    In the second example, delete [Symbol.toPrimitive]. Predict +price (hint: an object with no valueOf override converts through "[object Money]").

  3. 3

    Copy with spread

    In the first example, add const copy = { ...user }; console.log(copy[id]);. Does spread copy symbol keys?

Code & diagrams

Symbols as unique, hidden-from-JSON keys ES2015+ New tab

`description` is ES2019. The symbol key is skipped by `Object.keys` and JSON, but anyone can still list it with `getOwnPropertySymbols`.

Preview

Press Run to see the page.

Console

Nothing logged yet.

Expected console output

symbol
false
Symbol(id) id
42
[ 'name' ]
{"name":"Asha"}
[ Symbol(id) ]
[ 'name', Symbol(id) ]
true
app.id
undefined
Well-known symbols change built-in behaviour ES2015+ New tab

`+price`, `* 2` and `>` ask for a number; the template literal asks for a string; `+ "!"` gives the hint `"default"`, which this class treats like a string.

Preview

Press Run to see the page.

Console

Nothing logged yet.

Expected console output

250
500
250 INR
250 INR!
true
[object Money]
true false
3
Symbols as constants that can't be faked ES2015+ New tab

Mallory's symbol has the same description as `Status.ACTIVE`, but it's a different symbol, so it doesn't match. `String(u.status)` converts on purpose; `+` would throw.

Preview

Press Run to see the page.

Console

Nothing logged yet.

Expected console output

Asha can log in Symbol(active)
Ravi cannot log in Symbol(blocked)
Mallory unknown status Symbol(active)

Break it on purpose

Errors are the best teachers. Make each change, read the error, guess what went wrong, then reveal the answer.

Break #1

Join a symbol to a string

Write const id = Symbol("id"); console.log("Key: " + id);

terminal
$ Console
── what you'll see ──
Uncaught TypeError: Cannot convert a Symbol value to a string

Break #2

Call Symbol with new

Write const s = new Symbol("id");

terminal
$ Console
── what you'll see ──
Uncaught TypeError: Symbol is not a constructor

Break #3

Expect symbol keys in JSON

Store important data under a symbol key and send the object with JSON.stringify.

terminal
$ Console
── what you'll see ──
{"name":"Asha"}

Myth vs fact

Myth

Symbol-keyed properties are private.

Fact

They're hidden from the common listing methods, but Object.getOwnPropertySymbols, Reflect.ownKeys and DevTools show them. For privacy use #private fields or a WeakMap.

Myth

Two symbols with the same description are equal.

Fact

Only Symbol.for(key) symbols with the same key are the same. Every Symbol() call makes a new, unique value.

Myth

Symbols are rarely used, so you can skip them.

Fact

Every for...of, every spread and every instanceof goes through a well-known symbol. Understanding them explains how the language's protocols work.

Pro corner

Extra depth for experienced readers. New to this? Skip it for now and come back later.

  • ▸

    Well-known symbols are shared across realms (iframes, workers): Symbol.iterator in an iframe is the same value as in the parent page. That's why an array from an iframe is still iterable in the parent, even though instanceof Array fails across realms.

  • ▸

    Symbol.unscopables exists because adding Array.prototype.values in ES2015 broke old code using with (array) { values }. The array's [Symbol.unscopables] object lists methods that with must ignore, a direct example of "don't break the web".

  • ▸

    Symbol.species lets subclasses control which constructor methods like map use for their results. TC39 regrets it (it slows engines and caused security bugs), so avoid relying on it in new code.

  • ▸

    Since ES2023, non-registered symbols can be WeakMap keys (Topic 14.4). Registered Symbol.for symbols are never collected, so they can't be.

Remember this

  1. 1

    Symbol("id") creates a new symbol, a primitive type (like string or number; typeof gives "symbol", see Topic 1.2). The text is only a description for debugging (sym.description, sym.toString() gives "Symbol(id)"). Every call makes a value that is unique: Symbol("id") === Symbol("id") is false. Symbol is not a constructor: new Symbol() throws.

  2. 2

    Symbols are mainly used as property keys: obj[id] = 42 or in a literal { [id]: 42 } (square brackets make a computed key). Symbol-keyed properties are skipped by Object.keys, for...in, JSON.stringify and Object.entries. They're not secret, though: Object.getOwnPropertySymbols(obj) and Reflect.ownKeys(obj) list them, and object spread {...obj} and Object.assign copy them. Think collision-free, not private (for real privacy use #private fields, Topic 7.6).

  3. 3

    Symbols don't turn into strings by themselves: "id: " + sym throws TypeError: Cannot convert a Symbol value to a string, which protects you from using a symbol as text by accident. Convert on purpose with String(sym) or sym.description. Template literals throw too.

  4. 4

    The global symbol registry is a shared table across all the scripts of your page (iframes on the page share it too). Symbol.for("app.id") returns the symbol registered under that string, creating it the first time, so two separate scripts get the same symbol. Symbol.keyFor(sym) gives the registry key back (or undefined for a non-registered symbol). Use namespaced keys like "mylib.version" to avoid clashes.

  5. 5

    Well-known symbols are symbols stored as properties of Symbol that the language itself looks up. Defining them on your objects changes built-in behaviour: Symbol.iterator (what for...of uses, Topic 14.1), Symbol.asyncIterator (for await...of), Symbol.toPrimitive (how the object converts to a number or string), Symbol.toStringTag (the name in Object.prototype.toString, "[object Name]"), Symbol.hasInstance (what instanceof checks), and Symbol.isConcatSpreadable (whether concat flattens it). Others are used by strings and regex: Symbol.match, Symbol.replace, Symbol.split, Symbol.search, plus Symbol.species and Symbol.unscopables.

  6. 6

    Symbol.toPrimitive is the most useful to know. When an object is used where a primitive is needed, the engine calls obj[Symbol.toPrimitive](hint) with a hint: "number" (for +obj, obj * 2, comparisons like <), "string" (for String(obj) and template literals) or "default" (for obj + "" and ==). Without it, the engine falls back to valueOf() and toString(). Date uses this to return a string for + but a number for -.

Explain it without notes

01

What is a symbol and what problem does it solve?

02

What is the difference between Symbol("x") and Symbol.for("x")?

03

Which operations see symbol-keyed properties and which skip them?

04

What is Symbol.toPrimitive, and what are its three hints?

Practice

01

Create a Temperature class whose instances give their Celsius number for +t and "21°C" in a template literal. Log both for new Temperature(21).

02

Add a hidden createdBy tag to an object with a symbol key, then show that JSON.stringify doesn't include it but direct access does.

Trade-offs

  • ↔

    Symbol keys avoid name clashes and keep metadata out of JSON and loops, but they are awkward to debug, don't survive serialisation, and aren't truly private.

  • ↔

    Well-known symbol hooks make your objects feel built-in, but overriding conversions or instanceof can surprise readers; use them where the behaviour is natural (iteration, money formatting), not for clever tricks.

Done when you can

  • I can create symbols and explain why each is unique.

  • I can use a symbol as a property key and know which operations skip it.

  • I can use Symbol.for and Symbol.keyFor.

  • I can name the main well-known symbols and what they control.

  • I can implement Symbol.toPrimitive with the right hints.