Symbol in JavaScript
Symbol is a primitive data type introduced in ES6 that represents a unique and immutable value. Every call to Symbol() creates a new unique value even when the description is the same, which is why symbols are used as property keys that are guaranteed not to clash with anything.
Theory
TL;DR
Symbol()creates a unique immutable value; two symbols with the same description are not equal.- The main use is unique object property keys with no risk of a name collision.
- Symbol keys do not appear in
for...in,Object.keys()orJSON.stringify(). - They can still be retrieved with
Object.getOwnPropertySymbols(), so they are "hidden" rather than private fields. - Well-known symbols (
Symbol.iterator,Symbol.toPrimitiveand others) change the behaviour of built-in operations. Symbol.for(key)takes a symbol from the global registry, so such symbols can be compared across modules.
Quick example
const id1 = Symbol('user');
const id2 = Symbol('user');
console.log(id1 === id2); // false
const ID = Symbol('id');
const user = {
name: 'Tim',
[ID]: 1
};
console.log(user[ID]); // 1
console.log(Object.keys(user)); // ["name"]What Symbol is and why it is unique
A symbol is created by calling the function:
const id = Symbol();Every call to Symbol() creates a new unique value, even when the descriptions match:
const id1 = Symbol('user');
const id2 = Symbol('user');
console.log(id1 === id2); // falseAlthough both symbols carry the same description 'user', they are different. The description is only a debugging hint, it has no effect on equality.
What Symbol is for
Symbols are most often used as unique object property keys, to avoid name collisions.
Ordinary keys do collide:
const user = { name: 'Tim' };
user.id = 1;
user.id = 2; // overwrittenWith a symbol that cannot happen:
const ID = Symbol('id');
const user = {
name: 'Tim',
[ID]: 1
};
console.log(user[ID]); // 1Now the [ID] property is:
- unique;
- invisible during ordinary property enumeration.
Symbols do not take part in ordinary enumeration
for (let key in user) {
console.log(key);
}
// "name"console.log(Object.keys(user)); // ["name"]A symbol does not show up in for...in, Object.keys() or JSON.stringify(). But it can still be obtained:
console.log(Object.getOwnPropertySymbols(user)); // [Symbol(id)]Well-known symbols
JavaScript has built-in symbols, special values that let you change the behaviour of built-in operations.
| Symbol | What it is used for |
|---|---|
Symbol.iterator | makes an object iterable (for...of) |
Symbol.toPrimitive | customises conversion to a primitive |
Symbol.toStringTag | sets the string returned by Object.prototype.toString |
Symbol.hasInstance | overrides the behaviour of instanceof |
Symbol.asyncIterator | enables for await...of |
An example with Symbol.iterator:
const arr = [1, 2, 3];
const iterator = arr[Symbol.iterator]();
console.log(iterator.next()); // { value: 1, done: false }It is Symbol.iterator that makes an array "iterable", and that is exactly what makes for...of and spread (...arr) work.
An example with Symbol.toPrimitive:
const user = {
name: 'Tim',
age: 25,
[Symbol.toPrimitive](hint) {
return hint === 'string' ? this.name : this.age;
}
};
console.log(String(user)); // "Tim"
console.log(+user); // 25The symbol lets you control how an object is converted to a number or a string.
The global registry and "hidden" keys
Sometimes you need a symbol with a given name to be the same symbol everywhere. That is what the global symbol registry is for:
const id1 = Symbol.for('user');
const id2 = Symbol.for('user');
console.log(id1 === id2); // trueSymbol.for(key) looks the symbol up in the global registry and, when it is not found, creates a new one and stores it under that key. Symbol.keyFor() does the reverse:
Symbol.keyFor(id1); // 'user'Symbols are a good fit for:
- creating "private" properties in libraries and classes;
- extending objects you do not own without risking their data;
- metadata, for example in frameworks or an ORM.
An example of use in a class:
const _id = Symbol('id');
class User {
constructor(name) {
this.name = name;
this[_id] = Math.random();
}
}
const user = new User('Tim');
console.log(user); // { name: 'Tim', [Symbol(id)]: 0.123... }
console.log(Object.keys(user)); // ["name"]The symbol makes the _id field invisible to ordinary iteration and protects it from accidental overwriting.
The summary table:
| Property | Description |
|---|---|
| Type | a primitive |
| Uniqueness | every Symbol() is unique |
| Used as | a key for object properties |
| Not visible | in for...in, Object.keys(), JSON.stringify() |
| Global registry | through Symbol.for() and Symbol.keyFor() |
| Examples of built-in symbols | Symbol.iterator, Symbol.toPrimitive, Symbol.hasInstance and others |
Common mistakes
- Expecting
Symbol('id') === Symbol('id'). The description does not make symbols equal, every call produces a new symbol. - Treating symbol fields as genuinely private. They are visible through
Object.getOwnPropertySymbols()andReflect.ownKeys(); for real privacy there are#privateFielddeclarations. - Calling
Symbolwithnew. That is aTypeError, becauseSymbolis a primitive, not a constructor. - Concatenating a symbol with a string (
'id: ' + sym). The implicit conversion throws aTypeError, you needString(sym)orsym.description. - Confusing
Symbol()withSymbol.for(). The first is always unique, the second shares a value through the global registry. - Counting on symbol fields to survive
JSON.stringify()orstructuredClone(). They simply disappear.
Short Answer
Interview readyA concise answer to help you respond confidently on this topic during an interview.