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.iteratorthat the language looks for to decide how to treat an object. - Hint
- The word (
"number","string"or"default") the engine passes toSymbol.toPrimitiveto 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.
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.
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.
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
Make it a registry symbol
In the third example, change both
Symbol("active")calls toSymbol.for("active"). Predict what Mallory gets now, and explain why that weakens the constant. - 2
Remove the toPrimitive method
In the second example, delete
[Symbol.toPrimitive]. Predict+price(hint: an object with novalueOfoverride converts through"[object Money]"). - 3
Copy with spread
In the first example, add
const copy = { ...user }; console.log(copy[id]);. Does spread copy symbol keys?
Code & diagrams
`description` is ES2019. The symbol key is skipped by `Object.keys` and JSON, but anyone can still list it with `getOwnPropertySymbols`.
Preview
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`+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
Console
Nothing logged yet.Expected console output
250
500
250 INR
250 INR!
true
[object Money]
true false
3Mallory'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
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);
Break #2
Call Symbol with new
Write const s = new Symbol("id");
Break #3
Expect symbol keys in JSON
Store important data under a symbol key and send the object with JSON.stringify.
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.iteratorin 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 thoughinstanceof Arrayfails across realms. - ▸
Symbol.unscopablesexists because addingArray.prototype.valuesin ES2015 broke old code usingwith (array) { values }. The array's[Symbol.unscopables]object lists methods thatwithmust ignore, a direct example of "don't break the web". - ▸
Symbol.specieslets subclasses control which constructor methods likemapuse 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.forsymbols are never collected, so they can't be.
Remember this
- 1
Symbol("id")creates a new symbol, a primitive type (like string or number;typeofgives"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")isfalse.Symbolis not a constructor:new Symbol()throws. - 2
Symbols are mainly used as property keys:
obj[id] = 42or in a literal{ [id]: 42 }(square brackets make a computed key). Symbol-keyed properties are skipped byObject.keys,for...in,JSON.stringifyandObject.entries. They're not secret, though:Object.getOwnPropertySymbols(obj)andReflect.ownKeys(obj)list them, and object spread{...obj}andObject.assigncopy them. Think collision-free, not private (for real privacy use#privatefields, Topic 7.6). - 3
Symbols don't turn into strings by themselves:
"id: " + symthrowsTypeError: Cannot convert a Symbol value to a string, which protects you from using a symbol as text by accident. Convert on purpose withString(sym)orsym.description. Template literals throw too. - 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 (orundefinedfor a non-registered symbol). Use namespaced keys like"mylib.version"to avoid clashes. - 5
Well-known symbols are symbols stored as properties of
Symbolthat the language itself looks up. Defining them on your objects changes built-in behaviour:Symbol.iterator(whatfor...ofuses, Topic 14.1),Symbol.asyncIterator(for await...of),Symbol.toPrimitive(how the object converts to a number or string),Symbol.toStringTag(the name inObject.prototype.toString,"[object Name]"),Symbol.hasInstance(whatinstanceofchecks), andSymbol.isConcatSpreadable(whetherconcatflattens it). Others are used by strings and regex:Symbol.match,Symbol.replace,Symbol.split,Symbol.search, plusSymbol.speciesandSymbol.unscopables. - 6
Symbol.toPrimitiveis the most useful to know. When an object is used where a primitive is needed, the engine callsobj[Symbol.toPrimitive](hint)with a hint:"number"(for+obj,obj * 2, comparisons like<),"string"(forString(obj)and template literals) or"default"(forobj + ""and==). Without it, the engine falls back tovalueOf()andtoString().Dateuses this to return a string for+but a number for-.
Explain it without notes
What is a symbol and what problem does it solve?
What is the difference between Symbol("x") and Symbol.for("x")?
Which operations see symbol-keyed properties and which skip them?
What is Symbol.toPrimitive, and what are its three hints?
Practice
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).
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
instanceofcan 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.forandSymbol.keyFor.I can name the main well-known symbols and what they control.
I can implement
Symbol.toPrimitivewith the right hints.