Symbol type
An object property key can only ever be one of two types. The specification is strict about it:
- a string, or
- a symbol.
Hand JavaScript anything else and it quietly converts to a string first. A number key becomes its string form, so obj[1] reaches the very same slot as obj["1"]. Even a boolean gets stringified: obj[true] is obj["true"].
So far every key you’ve used has been a string. Time to meet the other legal key type: the symbol.
Symbols
A symbol is a unique identifier. That word “unique” is the whole point, so hold onto it.
You create one by calling Symbol():
let id = Symbol();
You can attach a description to a symbol when you make it (sometimes called the symbol name). It exists mainly to help you when you’re reading logs or debugging:
// id is a symbol, and its description reads "id"
let id = Symbol("id");
Here’s the guarantee that makes symbols worth having: every symbol you create is distinct from every other one. Make a hundred symbols all described as "id" and you get a hundred different values. The description is a sticky note, not an identity. It changes nothing about equality.
let id1 = Symbol("id");
let id2 = Symbol("id");
alert(id1 == id2); // false
If you’ve written Ruby or another language with a “symbol” concept, drop those instincts here. JavaScript symbols behave differently, and the resemblance is mostly the name.
To put it plainly: a symbol is a primitive value that is guaranteed unique and can carry an optional description. Mint a few below and watch: no matter how many Symbol("id") you create, none is ever equal to another.
Now let’s look at what that uniqueness buys you.
“Hidden” properties
Symbols let you tuck a property into an object where no unrelated code can trip over it, read it by accident, or clobber it.
Picture this: you’re handed user objects that come from someone else’s code, and you want to tag each one with an identifier of your own.
Use a symbol as the key:
let user = { // belongs to another code
name: "Maya"
};
let id = Symbol("id");
user[id] = 1;
alert( user[id] ); // read it back with the symbol as key
Why bother with Symbol("id") when the string "id" would also work as a key?
Because user belongs to another codebase, bolting plain string fields onto it is risky. You might collide with a property that code already relies on, or one it adds later. A symbol sidesteps that entirely. The other code has no reference to your symbol, so it can neither see nor overwrite your addition.
Now suppose a second script also wants to stamp its own identifier on the same user. It makes its own symbol:
// ...
let id = Symbol("id");
user[id] = "Their id value";
No conflict. Their Symbol("id") and your Symbol("id") are different values, so they land in different slots even though both descriptions read "id".
Had both scripts used the plain string "id", the second write would have silently stomped the first:
let user = { name: "Maya" };
// Our script writes to the "id" property
user.id = "Our id value";
// ...another script wants "id" too...
user.id = "Their id value"
// Boom! our value is gone
Symbols in an object literal
To use a symbol as a key inside an object literal {...}, wrap it in square brackets.
let id = Symbol("id");
let user = {
name: "Maya",
[id]: 123 // not "id": 123
};
The brackets tell JavaScript to evaluate id and use its value (the symbol) as the key. Without them you’d get a plain string key literally named "id", which is the opposite of what you want.
Symbols are skipped by for..in
Symbolic properties sit out the for..in loop entirely.
let id = Symbol("id");
let user = {
name: "Maya",
age: 30,
[id]: 123
};
for (let key in user) alert(key); // name, age (no symbols)
// direct access by the symbol still works fine
alert( "Direct: " + user[id] ); // Direct: 123
Object.keys(user) leaves them out too. It’s the same “keep symbolic properties hidden” principle at work: if a library or some unrelated script iterates over your object, it won’t stumble into a symbolic property it never knew existed.
user = { name, age, [id] }
Try it live below. Change the hidden value, re-run, and notice the symbol key never shows up in for..in or Object.keys — yet direct access always reaches it.
Object.assign, by contrast, copies both string keys and symbol keys:
let id = Symbol("id");
let user = {
[id]: 123
};
let clone = Object.assign({}, user);
alert( clone[id] ); // 123
No contradiction there. It’s intentional. When you clone or merge objects, the usual intent is to carry over everything the object holds, symbols included.
Global symbols
Normally, as you’ve seen, two symbols are never equal even when they share a description. Sometimes that’s the wrong behavior. Sometimes you want separate parts of an application to reach for a symbol "id" and mean the same property everywhere.
For that there’s a global symbol registry. You register symbols in it by name, and every lookup with the same name hands back the very same symbol.
Read from it (creating the symbol if it isn’t there yet) with Symbol.for(key). The call checks the registry: if a symbol described as key already exists, you get it back; if not, one is created, stored under that key, and returned.
// read from the global registry
let id = Symbol.for("id"); // created here, since it didn't exist yet
// read it again — perhaps from a totally different file
let idAgain = Symbol.for("id");
// same symbol both times
alert( id === idAgain ); // true
Symbols living in this registry are called global symbols. When you need one identifier that any code, anywhere in your app, can look up and agree on, that’s the tool.
Type any name below and compare the two roads: Symbol.for(name) always returns the same shared symbol, while a plain Symbol(name) is a private one-off.
Symbol.keyFor
Symbol.for(key) goes from a name to a symbol. To travel the other direction, from a global symbol back to its name, use Symbol.keyFor(sym):
// get symbols by name
let sym = Symbol.for("name");
let sym2 = Symbol.for("id");
// get names by symbol
alert( Symbol.keyFor(sym) ); // name
alert( Symbol.keyFor(sym2) ); // id
Symbol.keyFor works by searching the global registry. That’s the catch: it only knows about global symbols. Give it an ordinary (non-registered) symbol and the lookup fails, returning undefined:
let globalSymbol = Symbol.for("name");
let localSymbol = Symbol("name");
alert( Symbol.keyFor(globalSymbol) ); // name, it's in the registry
alert( Symbol.keyFor(localSymbol) ); // undefined, not global
alert( localSymbol.description ); // name
Note the last line: description is available on every symbol, global or not. Symbol.keyFor is the registry-only counterpart.
System symbols
JavaScript keeps a set of built-in symbols of its own, the well-known symbols, which it consults internally. You can define properties under these keys to fine-tune how your objects behave in various built-in operations.
The specification lists them in its well-known symbols table:
Symbol.hasInstanceSymbol.isConcatSpreadableSymbol.iteratorSymbol.toPrimitive- …and more.
Symbol.toPrimitive, for one, lets you dictate how an object converts to a primitive. You’ll put it to work shortly. Symbol.iterator defines how an object behaves in a for..of loop. The rest will click into place as you meet the language features that use them.
Summary
Symbol is a primitive type whose whole job is producing unique identifiers.
You create one with Symbol(), optionally passing a description (its name). Any two symbols are different values, even with matching descriptions. When you do want same-named symbols to be equal, reach for the global registry: Symbol.for(key) returns (or creates) a global symbol named key, and every call with the same key yields the identical symbol.
Two use cases carry most of the weight:
-
“Hidden” object properties. To attach a property to an object that belongs to another script or library, use a symbol as the key. It won’t surface in
for..inorObject.keys, so unrelated code won’t process it by accident, and since that code lacks your symbol, it can’t read or overwrite the property either. You get to stash data where you need it without other code seeing it. -
System symbols. JavaScript exposes many built-in behaviors through
Symbol.*. Define matching properties to reshape them. Later you’ll useSymbol.iteratorfor iterables andSymbol.toPrimitivefor object-to-primitive conversion, among others.
One honest caveat: symbols aren’t a hard privacy wall. The built-in Object.getOwnPropertySymbols(obj) returns every symbol key on an object, and Reflect.ownKeys(obj) returns all keys, symbolic ones included. What symbols give you is protection from accidents — most libraries, built-ins, and syntax simply don’t reach for those two methods. For truly private state you’d want private class fields (#field) instead.