In JavaScript, what is a Symbol, and what does using a symbol as an object property key give you that a string key does not?
answer
- a primitive, not an object
- every call makes a new value
- description is only a label
- collision-proof property keys
- hidden from loops, not from getOwnPropertySymbols
basics
~20 sA Symbol is a primitive whose every value is unique: Symbol('id') never equals another Symbol('id'). Used as a property key, it cannot collide with any string key or with symbols created by other code, even when the descriptions match.
solid answer
~40 s`Symbol` is one of JavaScript's primitive types, created by calling `Symbol()` as a function with an optional description string. Every call returns a brand-new value, so `Symbol('id') === Symbol('id')` is `false`, and `typeof` on one returns `'symbol'`. `Symbol` is not a constructor — `new Symbol()` throws a `TypeError`. Because each symbol is unique, a symbol property key can never clash with a string key or with a symbol another library made, which is why you use one to attach metadata to an object you do not own. You read the description back with `sym.description` (added in ES2019) and write the key with computed-key syntax: `{ [ID]: 1 }` and `obj[ID]`. Symbols hide a property from ordinary enumeration, but they are not access control — `Object.getOwnPropertySymbols` still finds them.
go deeper
Be able to say that a symbol is a unique primitive, that Symbol('a') === Symbol('a') is false, and that a symbol used as a property key cannot collide with anyone else's key.
Explain the mechanics: Symbol is not a constructor, keys must be written with computed-key brackets, and symbol keys are skipped by Object.keys, for...in and JSON.stringify but returned by Object.getOwnPropertySymbols.
Show judgment about when a symbol key is the right tool — annotating objects you do not own, keeping framework bookkeeping out of user-visible loops — and warn clearly that it buys collision-safety, not confidentiality or serialisation safety.
Own the API-design angle: choosing between a symbol key, a WeakMap side table and a documented string key changes who can discover the data, whether it survives cloning and serialisation, and how a library's internals leak into consumers' object graphs.
## What a symbol actually is A *symbol* is a primitive value, sitting alongside string, number, boolean, `null`, `undefined` and bigint in the language's set of primitives. You create one by calling the global `Symbol` function, optionally passing a **description** — a human-readable label used only for debugging. ```js const a = Symbol('id'); const b = Symbol('id'); a === b; // false — every call makes a new value typeof a; // 'symbol' a.description; // 'id' (ES2019 accessor) a.toString(); // 'Symbol(id)' ``` The description plays no role in equality. Two symbols are the same value only when they are literally the same symbol — the one produced by one particular call. That is the whole point: a symbol is a value you can hand out, and nobody can forge another one that compares equal to it. ## Symbol is a function, not a constructor ```js new Symbol('id'); // TypeError: Symbol is not a constructor ``` The spec deliberately blocks `new` here. Wrapper objects around primitives are already a source of confusion, and a `Symbol` object that is truthy but not `===` to the primitive it wraps would be worse. If you ever genuinely need the wrapper (rarely), `Object(sym)` produces it. ## Why a unique key is useful Property keys in JavaScript are strings or symbols — nothing else. (A number key like `obj[1]` is converted to the string `'1'`.) With string keys, any two pieces of code that pick the same name write to the same slot: ```js // library A obj.id = 42; // library B, later, in the same object obj.id = 'user-7'; // silently clobbers A ``` With a symbol, each library holds its own key and neither can see or overwrite the other's: ```js const ID = Symbol('id'); // module-private value let counter = 0; export function tag(obj) { obj[ID] = ++counter; } export function idOf(obj) { return obj[ID]; } ``` This is the everyday use: annotating objects you do not own — DOM-free plain objects passed through a framework, instances from a third-party class, an options bag threaded through several layers — without adding a name anyone else might also pick. ## Syntax: symbol keys are always computed Because the key is a *value*, not an identifier, you cannot write `obj.ID`. Every use goes through brackets, including in object literals and class bodies: ```js const ID = Symbol('id'); const user = { [ID]: 1, name: 'Ada' }; user[ID]; // 1 user.ID; // undefined — this is the string key 'ID' class Repo { [ID]() { return 'method with a symbol key'; } } ``` That also means you must keep the symbol itself around. A symbol you created and then dropped on the floor makes its property effectively unreachable — which is occasionally a feature and more often a bug. ## Hidden from enumeration, but not private Symbol-keyed properties are skipped by `for...in`, `Object.keys`, `Object.values`, `Object.entries`, `Object.getOwnPropertyNames` and `JSON.stringify`. That keeps your annotation out of everybody's loops and serialised payloads. It is *not* an access-control mechanism: `Object.getOwnPropertySymbols(obj)` returns the symbol keys, and `Reflect.ownKeys(obj)` returns strings and symbols together. Anyone with a reference to the object can enumerate and read them. ```js const s = Symbol('secret'); const o = { [s]: 'value', open: 1 }; Object.keys(o); // ['open'] JSON.stringify(o); // '{"open":1}' Object.getOwnPropertySymbols(o); // [ Symbol(secret) ] ``` If you want a member outsiders genuinely cannot reach, symbols are the wrong tool; they solve *collision*, not *confidentiality*. ## Two flavours beyond plain Symbol() The language adds two structured sources of symbols on top of anonymous `Symbol()` values. `Symbol.for(key)` looks a symbol up in a process-wide registry, returning the same symbol for the same string every time — the opposite of uniqueness, chosen when two independent copies of code must agree on one key. And the *well-known symbols* — `Symbol.iterator`, `Symbol.asyncIterator`, `Symbol.toPrimitive`, `Symbol.toStringTag`, `Symbol.hasInstance`, `Symbol.species` and a handful more — are fixed symbols the specification itself looks up on your objects, so that defining one lets your type plug into a built-in operation. ## Cost and gotchas Symbols are cheap; they are ordinary primitive values. The practical gotchas are: they vanish from JSON, so anything you need to survive serialisation must live under a string key; they cannot be implicitly concatenated into a string (`'x' + sym` throws); and `Object.assign` and object spread *do* copy own enumerable symbol keys, so a symbol annotation survives a shallow clone even though it disappeared from `Object.keys`.
- If two symbols with the same description are never equal, what is the description actually for?Debugging only. It shows up in `sym.toString()` as `Symbol(id)`, in devtools output, and via the `sym.description` accessor added in ES2019. It takes no part in equality and no part in property lookup — two symbols made with the same description are still different keys. Treat it like a variable name you can read at runtime.
- Can a symbol ever be coerced to a string implicitly?No. Implicit string coercion of a symbol throws a `TypeError`, so `'id: ' + sym` and `` `${sym}` `` both fail. You must ask explicitly: `String(sym)` and `sym.toString()` both return `'Symbol(id)'`. The spec made this loud on purpose, because a silently stringified symbol would quietly become an ordinary string key.
- Do symbol-keyed properties survive Object.assign or object spread?Yes. Both copy own *enumerable* properties, and that includes symbol keys — `{ ...obj }` carries your symbol annotation into the clone. This surprises people who assume that anything invisible to `Object.keys` is also invisible to spread. What does not survive is `JSON.parse(JSON.stringify(obj))`, since JSON has no notion of symbols at all.
saying these in an interview costs you the question
- Says two symbols with the same description are equal
- Calls new Symbol('id') to create one
- Claims symbol properties are private and unreachable
- Expects symbol-keyed data to survive JSON.stringify
- Thinks symbols are objects rather than primitives