AI InterviewSearch…Search problems, topics, or pages…Ctrl KGet started
Skip to content

06 Collections and iteration / Lesson 5 of 5

Symbols — unique keys and the language's extension points

11 min · +10 XP · 2 exercises · 5 questions

Read the text11 min
Exercises0/2
Quiz5 q

Quick answer

What is a Symbol in JavaScript?

A symbol is a JavaScript primitive whose only job is to be unique — Symbol("id") === Symbol("id") is false. Used as a property key it cannot collide with any other key, and it is skipped by Object.keys, for…in and JSON.stringify. The language also uses well-known symbols such as Symbol.iterator as hooks that your objects can implement.

A symbol is a primitive whose only job is to be unique: Symbol("id") !== Symbol("id"). Used as a property key it cannot collide with any string key or any other symbol, it is skipped by Object.keys, for…in and JSON.stringify, and it lets two pieces of code attach data to the same object without knowing about each other. The language itself uses a fixed set of well-known symbols as hook names — Symbol.iterator is the one you have been using all module — so that your objects can plug into for…of, instanceof, string conversion and more without the language reserving any string names. This lesson covers the primitive, the global registry, symbols as keys, and the well-known symbols worth knowing by name.

The primitive

const id = Symbol("user id");           // the string is a description for debugging only
typeof id;                              // "symbol"
id.description;                         // "user id"
id.toString();                          // "Symbol(user id)"
String(id);                             // same; `${id}` and id + "" THROW — no implicit string conversion
Symbol("x") === Symbol("x");            // false — every call is a new symbol

Symbols cannot be newed (new Symbol() throws), are never coerced to strings implicitly (a guard against accidentally turning a key into "Symbol(x)"), and are compared by identity. The seventh primitive type, after string, number, bigint, boolean, undefined, null.

Symbols as property keys

const HIDDEN = Symbol("hidden");
const obj = { visible: 1, [HIDDEN]: "secret" };   // computed key syntax
obj[HIDDEN];                                    // "secret" — must use brackets; obj.HIDDEN is the string key "HIDDEN"

Object.keys(obj);                     // ["visible"]
JSON.stringify(obj);                  // {"visible":1}
for (const k in obj) { }              // visible only
Object.getOwnPropertySymbols(obj);    // [Symbol(hidden)]
Reflect.ownKeys(obj);                 // ["visible", Symbol(hidden)] — everything, strings first
{ ...obj }                            // copies symbol keys too (own enumerable, any type)
Object.assign({}, obj)                // same

Symbol-keyed properties are not private — anyone with Object.getOwnPropertySymbols or the symbol itself can read them. They are collision-free and invisible to the common enumeration paths, which is a different, useful guarantee: a library can stamp obj[Symbol("cached")] on your objects without ever clashing with your fields or leaking into your JSON. For true privacy, #fields or a WeakMap.

The global registry: Symbol.for

Symbol.for("app.id") === Symbol.for("app.id");   // true — looked up (or created) in a process-wide registry
Symbol.keyFor(Symbol.for("app.id"));             // "app.id"
Symbol.keyFor(Symbol("local"));                  // undefined — not registered

Symbol.for(key) returns the same symbol for the same string everywhere in the realm — across modules, across bundles, across iframes. Use it when two independently loaded pieces of code must agree on a key (Node's own Symbol.for("nodejs.util.inspect.custom") is the canonical example). Use plain Symbol() when uniqueness is the point.

Well-known symbols

Built-in symbols stored as static properties of Symbol; the language looks them up on your objects at specific moments:

SymbolThe language calls it when…You implement
Symbol.iteratorfor…of, spread, destructuring, Array.froma method returning an iterator (usually a generator)
Symbol.asyncIteratorfor await…ofan async generator or async iterator
Symbol.toPrimitiveconversion to a primitive (+, ${}, ==)(hint) => primitive — overrides valueOf/toString
Symbol.toStringTagObject.prototype.toString.call(x)a getter returning the tag, giving [object Tag]
Symbol.hasInstancex instanceof Cstatic [Symbol.hasInstance](x) — custom instanceof
Symbol.speciesarray/promise methods creating derived objectsa static getter returning the constructor to use
Symbol.isConcatSpreadablearr.concat(x)true to spread an array-like, false to keep an array whole
Symbol.match/replace/search/splitstring methods given a non-RegExpobjects that act like patterns
Symbol.unscopableswith statementsignore
class Even {
  static [Symbol.hasInstance](n) { return Number.isInteger(n) && n % 2 === 0; }
}
4 instanceof Even;      // true — no Even object was ever created

const arrayLike = { length: 2, 0: "a", 1: "b", [Symbol.isConcatSpreadable]: true };
[1].concat(arrayLike);  // [1, "a", "b"]

You will implement Symbol.iterator often, Symbol.toPrimitive/toStringTag sometimes, hasInstance and species almost never — but recognising them explains behaviour that otherwise looks magical (why Stack extends Array methods return Stacks: species).

Enums and sentinels

Symbols make excellent sentinel values — const MISSING = Symbol("missing") can never be confused with a real value the way null or -1 can — and readable enums (const Color = Object.freeze({ Red: Symbol("red"), Green: Symbol("green") })) whose members cannot be forged from strings. The cost: symbols do not serialise, so anything that crosses JSON needs a string mapping.

Common mistakes

  • obj.SYM instead of obj[SYM] (creates a string-keyed property).
  • Expecting JSON.stringify or Object.keys to show symbol keys — or, the reverse, expecting symbol keys to be private.
  • ` ${sym} — throws; use sym.description or String(sym)`.
  • Using Symbol() where two modules need the same key (use Symbol.for), or Symbol.for where uniqueness matters.
  • Storing symbols in something that will be serialised.

Interview angle

  • "What is a symbol for?" A unique, collision-free property key hidden from ordinary enumeration; and the language's hook names (well-known symbols).
  • "Symbol() versus Symbol.for()?" Fresh and unique every call versus one shared symbol per string in a global registry.
  • "Are symbol properties private?" No — hidden from keys/JSON/for…in, visible via getOwnPropertySymbols/Reflect.ownKeys.
  • "Name three well-known symbols." iterator, toPrimitive, hasInstance (or toStringTag, asyncIterator, species).
  • "How would you customise instanceof?" static [Symbol.hasInstance](x) on the class.

Key takeaways

  • Symbol(desc) is unique and never implicitly stringified; typeof is "symbol".
  • As keys: brackets to access; skipped by keys/for…in/JSON; found by getOwnPropertySymbols/Reflect.ownKeys; copied by spread.
  • Symbol.for shares one symbol per string across the realm; Symbol.keyFor reads it back.
  • Well-known symbols are hooks: iterator, asyncIterator, toPrimitive, toStringTag, hasInstance, species, isConcatSpreadable.
  • Symbols are ideal sentinels and enum members, and do not survive JSON.

Common questions

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

Symbol() creates a new, unique symbol on every call. Symbol.for(key) looks the key up in a global registry and returns the same symbol for the same string everywhere in the realm, across modules and bundles; Symbol.keyFor reads the key back from a registered symbol.

Are symbol properties private in JavaScript?

No. Symbol-keyed properties are skipped by Object.keys, for…in and JSON.stringify, but Object.getOwnPropertySymbols and Reflect.ownKeys list them, and spread copies them. They are collision-free, not private; use #private fields or a WeakMap for real privacy.

What are well-known symbols in JavaScript?

Well-known symbols are built-in symbols stored on Symbol that the language looks up on your objects at set moments: Symbol.iterator for for…of and spread, Symbol.asyncIterator for for await, Symbol.toPrimitive for conversion, Symbol.hasInstance for instanceof, plus Symbol.toStringTag, Symbol.species and others.

Why does converting a symbol to a string throw?

Symbols are never converted to strings implicitly, so a template literal like ${sym} or sym + "" throws a TypeError. That guards against a key silently turning into the text "Symbol(x)". Convert explicitly with String(sym), sym.toString() or sym.description.

Why use a symbol as a sentinel value?

A sentinel such as const MISSING = Symbol("missing") can never be confused with real data the way null or -1 can, and enum members made of symbols cannot be forged from strings. The cost is that symbols do not survive JSON.stringify, so anything crossing JSON needs a string mapping.

Mark as readSign in to keep your place.

What Is a Symbol in JavaScript? Well-Known Symbols

A JavaScript symbol is a unique primitive used as a collision-free property key. Symbol vs Symbol.for, what skips symbol keys, and the well-known symbols.

  • Course: JavaScript study plan
  • Module: Collections and iteration
  • Kind: Lesson
  • Reading time: 11 min
  • Runtime: Node 16

What is a Symbol in JavaScript?

A symbol is a JavaScript primitive whose only job is to be unique — Symbol("id") === Symbol("id") is false. Used as a property key it cannot collide with any other key, and it is skipped by Object.keys, for…in and JSON.stringify. The language also uses well-known symbols such as Symbol.iterator as hooks that your objects can implement.

Lesson

A symbol is a primitive whose only job is to be unique: Symbol("id") !== Symbol("id"). Used as a property key it cannot collide with any string key or any other symbol, it is skipped by Object.keys, for…in and JSON.stringify, and it lets two pieces of code attach data to the same object without knowing about each other. The language itself uses a fixed set of well-known symbols as hook names — Symbol.iterator is the one you have been using all module — so that your objects can plug into for…of, instanceof, string conversion and more without the language reserving any string names. This lesson covers the primitive, the global registry, symbols as keys, and the well-known symbols worth knowing by name.

The primitive

const id = Symbol("user id");           // the string is a description for debugging only
typeof id;                              // "symbol"
id.description;                         // "user id"
id.toString();                          // "Symbol(user id)"
String(id);                             // same; `${id}` and id + "" THROW — no implicit string conversion
Symbol("x") === Symbol("x");            // false — every call is a new symbol

Symbols cannot be newed (new Symbol() throws), are never coerced to strings implicitly (a guard against accidentally turning a key into "Symbol(x)"), and are compared by identity. The seventh primitive type, after string, number, bigint, boolean, undefined, null.

Symbols as property keys

const HIDDEN = Symbol("hidden");
const obj = { visible: 1, [HIDDEN]: "secret" };   // computed key syntax
obj[HIDDEN];                                    // "secret" — must use brackets; obj.HIDDEN is the string key "HIDDEN"

Object.keys(obj);                     // ["visible"]
JSON.stringify(obj);                  // {"visible":1}
for (const k in obj) { }              // visible only
Object.getOwnPropertySymbols(obj);    // [Symbol(hidden)]
Reflect.ownKeys(obj);                 // ["visible", Symbol(hidden)] — everything, strings first
{ ...obj }                            // copies symbol keys too (own enumerable, any type)
Object.assign({}, obj)                // same

Symbol-keyed properties are not private — anyone with Object.getOwnPropertySymbols or the symbol itself can read them. They are collision-free and invisible to the common enumeration paths, which is a different, useful guarantee: a library can stamp obj[Symbol("cached")] on your objects without ever clashing with your fields or leaking into your JSON. For true privacy, #fields or a WeakMap.

The global registry: Symbol.for

Symbol.for("app.id") === Symbol.for("app.id");   // true — looked up (or created) in a process-wide registry
Symbol.keyFor(Symbol.for("app.id"));             // "app.id"
Symbol.keyFor(Symbol("local"));                  // undefined — not registered

Symbol.for(key) returns the same symbol for the same string everywhere in the realm — across modules, across bundles, across iframes. Use it when two independently loaded pieces of code must agree on a key (Node's own Symbol.for("nodejs.util.inspect.custom") is the canonical example). Use plain Symbol() when uniqueness is the point.

Well-known symbols

Built-in symbols stored as static properties of Symbol; the language looks them up on your objects at specific moments:

SymbolThe language calls it when…You implement
Symbol.iteratorfor…of, spread, destructuring, Array.froma method returning an iterator (usually a generator)
Symbol.asyncIteratorfor await…ofan async generator or async iterator
Symbol.toPrimitiveconversion to a primitive (+, ${}, ==)(hint) => primitive — overrides valueOf/toString
Symbol.toStringTagObject.prototype.toString.call(x)a getter returning the tag, giving [object Tag]
Symbol.hasInstancex instanceof Cstatic [Symbol.hasInstance](x) — custom instanceof
Symbol.speciesarray/promise methods creating derived objectsa static getter returning the constructor to use
Symbol.isConcatSpreadablearr.concat(x)true to spread an array-like, false to keep an array whole
Symbol.match/replace/search/splitstring methods given a non-RegExpobjects that act like patterns
Symbol.unscopableswith statementsignore
class Even {
  static [Symbol.hasInstance](n) { return Number.isInteger(n) && n % 2 === 0; }
}
4 instanceof Even;      // true — no Even object was ever created

const arrayLike = { length: 2, 0: "a", 1: "b", [Symbol.isConcatSpreadable]: true };
[1].concat(arrayLike);  // [1, "a", "b"]

You will implement Symbol.iterator often, Symbol.toPrimitive/toStringTag sometimes, hasInstance and species almost never — but recognising them explains behaviour that otherwise looks magical (why Stack extends Array methods return Stacks: species).

Enums and sentinels

Symbols make excellent sentinel values — const MISSING = Symbol("missing") can never be confused with a real value the way null or -1 can — and readable enums (const Color = Object.freeze({ Red: Symbol("red"), Green: Symbol("green") })) whose members cannot be forged from strings. The cost: symbols do not serialise, so anything that crosses JSON needs a string mapping.

Common mistakes

  • obj.SYM instead of obj[SYM] (creates a string-keyed property).
  • Expecting JSON.stringify or Object.keys to show symbol keys — or, the reverse, expecting symbol keys to be private.
  • ` ${sym} — throws; use sym.description or String(sym)`.
  • Using Symbol() where two modules need the same key (use Symbol.for), or Symbol.for where uniqueness matters.
  • Storing symbols in something that will be serialised.

Interview angle

  • "What is a symbol for?" A unique, collision-free property key hidden from ordinary enumeration; and the language's hook names (well-known symbols).
  • "Symbol() versus Symbol.for()?" Fresh and unique every call versus one shared symbol per string in a global registry.
  • "Are symbol properties private?" No — hidden from keys/JSON/for…in, visible via getOwnPropertySymbols/Reflect.ownKeys.
  • "Name three well-known symbols." iterator, toPrimitive, hasInstance (or toStringTag, asyncIterator, species).
  • "How would you customise instanceof?" static [Symbol.hasInstance](x) on the class.

Key takeaways

  • Symbol(desc) is unique and never implicitly stringified; typeof is "symbol".
  • As keys: brackets to access; skipped by keys/for…in/JSON; found by getOwnPropertySymbols/Reflect.ownKeys; copied by spread.
  • Symbol.for shares one symbol per string across the realm; Symbol.keyFor reads it back.
  • Well-known symbols are hooks: iterator, asyncIterator, toPrimitive, toStringTag, hasInstance, species, isConcatSpreadable.
  • Symbols are ideal sentinels and enum members, and do not survive JSON.

Common questions

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

Symbol() creates a new, unique symbol on every call. Symbol.for(key) looks the key up in a global registry and returns the same symbol for the same string everywhere in the realm, across modules and bundles; Symbol.keyFor reads the key back from a registered symbol.

Are symbol properties private in JavaScript?

No. Symbol-keyed properties are skipped by Object.keys, for…in and JSON.stringify, but Object.getOwnPropertySymbols and Reflect.ownKeys list them, and spread copies them. They are collision-free, not private; use #private fields or a WeakMap for real privacy.

What are well-known symbols in JavaScript?

Well-known symbols are built-in symbols stored on Symbol that the language looks up on your objects at set moments: Symbol.iterator for for…of and spread, Symbol.asyncIterator for for await, Symbol.toPrimitive for conversion, Symbol.hasInstance for instanceof, plus Symbol.toStringTag, Symbol.species and others.

Why does converting a symbol to a string throw?

Symbols are never converted to strings implicitly, so a template literal like ${sym} or sym + "" throws a TypeError. That guards against a key silently turning into the text "Symbol(x)". Convert explicitly with String(sym), sym.toString() or sym.description.

Why use a symbol as a sentinel value?

A sentinel such as const MISSING = Symbol("missing") can never be confused with real data the way null or -1 can, and enum members made of symbols cannot be forged from strings. The cost is that symbols do not survive JSON.stringify, so anything crossing JSON needs a string mapping.

Exercises

String keys and symbol keys, side by side

Line 1 lists string keys (assign each its index), line 2 lists names for Symbol(name) keys (assign hidden-<i>). Build one object with both. Print keys=<Object.keys> json=<JSON.stringify>; symbols=<getOwnPropertySymbols count> descriptions=<their descriptions>; ownKeys=<Reflect.ownKeys length> spreadKeepsSymbols=<does { ...obj } keep them all>; then for the first symbol name: plainEqual=<Symbol(n) === Symbol(n)> forEqual=<Symbol.for(n) === Symbol.for(n)> keyFor=<Symbol.keyFor(Symbol.for(n))> keyForPlain=<Symbol.keyFor of your first plain symbol>; finally try to put the first symbol in a template literal and print template=<result or the error's constructor name> string=<String(sym)> typeof=<typeof sym>.

Example: name born / id secret →

keys=name,born json={"name":0,"born":1}
symbols=2 descriptions=id,secret
ownKeys=4 spreadKeepsSymbols=true
plainEqual=false forEqual=true keyFor=id keyForPlain=undefined
template=TypeError string=Symbol(id) typeof=symbol

Well-known symbols in action

Implement three hooks. Even gets static [Symbol.hasInstance](n) so 4 instanceof Even is true for even integers. Version(major, minor) gets [Symbol.toPrimitive](hint) — "number" → major + minor / 100, otherwise v<major>.<minor> — and get [Symbol.toStringTag]() returning "Version". Line 1 is numbers, line 2 a version like 1.4. Print even=<comma list> odd=<comma list>; then string=<template of v> number=<+v> default=<v + ""> compare=<v > 1> tag=<Object.prototype.toString.call(v)>; then build { length: 2, 0: "x", 1: "y", [Symbol.isConcatSpreadable]: true } and the same object without the symbol and print concatSpreadable=<JSON of [1].concat(spreadable)> concatPlain=<JSON of [1].concat(plain)>.

Example: 1 2 3 4 10 7 / 1.4 →

even=2,4,10 odd=1,3,7
string=v1.4 number=1.04 default=v1.4 compare=true tag=[object Version]
concatSpreadable=[1,"x","y"] concatPlain=[1,{"0":"x","1":"y","length":2}]

In this module: Collections and iteration

← Generators — functions that pause, and lazy pipelines · Checkpoint — Collections and iteration →