Symbols are unique, immutable primitive values used for property keys and metaprogramming. Here's how to use them effectively.
Basic Usage#
1// Create unique symbols
2const sym1 = Symbol();
3const sym2 = Symbol();
4
5console.log(sym1 === sym2); // false - always unique
6
7// With description (for debugging)
8const id = Symbol('id');
9const name = Symbol('name');
10
11console.log(id.description); // 'id'
12console.log(String(id)); // 'Symbol(id)'
13
14// Symbols as property keys
15const user = {
16 [id]: 123,
17 [name]: 'John',
18 email: 'john@example.com',
19};
20
21console.log(user[id]); // 123
22console.log(user[name]); // 'John'Hidden Properties#
1// Symbols are not enumerable by default
2const secret = Symbol('secret');
3
4const obj = {
5 visible: 'I am visible',
6 [secret]: 'I am hidden',
7};
8
9// Not included in iterations
10console.log(Object.keys(obj)); // ['visible']
11console.log(JSON.stringify(obj)); // '{"visible":"I am visible"}'
12for (const key in obj) console.log(key); // 'visible'
13
14// But accessible directly
15console.log(obj[secret]); // 'I am hidden'
16
17// Get symbol properties
18console.log(Object.getOwnPropertySymbols(obj)); // [Symbol(secret)]
19console.log(Reflect.ownKeys(obj)); // ['visible', Symbol(secret)]Symbol.for() - Global Registry#
1// Create/retrieve symbols from global registry
2const globalSym1 = Symbol.for('app.id');
3const globalSym2 = Symbol.for('app.id');
4
5console.log(globalSym1 === globalSym2); // true - same symbol
6
7// Get key from symbol
8console.log(Symbol.keyFor(globalSym1)); // 'app.id'
9
10// Regular symbols are not in registry
11const localSym = Symbol('local');
12console.log(Symbol.keyFor(localSym)); // undefined
13
14// Cross-realm symbol sharing
15// Symbol.for() returns same symbol across iframes, workers, etc.Well-Known Symbols#
1// Symbol.iterator - make objects iterable
2const range = {
3 start: 1,
4 end: 5,
5
6 [Symbol.iterator]() {
7 let current = this.start;
8 const end = this.end;
9
10 return {
11 next() {
12 if (current <= end) {
13 return { value: current++, done: false };
14 }
15 return { done: true };
16 },
17 };
18 },
19};
20
21console.log([...range]); // [1, 2, 3, 4, 5]
22
23for (const num of range) {
24 console.log(num); // 1, 2, 3, 4, 5
25}Symbol.toStringTag#
1// Customize Object.prototype.toString() result
2class MyClass {
3 get [Symbol.toStringTag]() {
4 return 'MyClass';
5 }
6}
7
8const obj = new MyClass();
9console.log(Object.prototype.toString.call(obj)); // '[object MyClass]'
10console.log(obj.toString()); // '[object MyClass]'
11
12// Built-in examples
13console.log(Object.prototype.toString.call(new Map())); // '[object Map]'
14console.log(Object.prototype.toString.call(new Set())); // '[object Set]'
15console.log(Object.prototype.toString.call(new Promise(() => {}))); // '[object Promise]'Symbol.toPrimitive#
1// Customize type coercion
2class Money {
3 constructor(amount, currency) {
4 this.amount = amount;
5 this.currency = currency;
6 }
7
8 [Symbol.toPrimitive](hint) {
9 switch (hint) {
10 case 'number':
11 return this.amount;
12 case 'string':
13 return `${this.currency}${this.amount}`;
14 default: // 'default'
15 return this.amount;
16 }
17 }
18}
19
20const price = new Money(100, '$');
21
22console.log(+price); // 100 (number hint)
23console.log(`${price}`); // '$100' (string hint)
24console.log(price + 50); // 150 (default hint)
25console.log(price == 100); // true (default hint)Symbol.hasInstance#
1// Customize instanceof behavior
2class MyArray {
3 static [Symbol.hasInstance](instance) {
4 return Array.isArray(instance);
5 }
6}
7
8console.log([] instanceof MyArray); // true
9console.log([1, 2] instanceof MyArray); // true
10console.log({} instanceof MyArray); // false
11
12// Practical example
13class Validator {
14 constructor(predicate) {
15 this.predicate = predicate;
16 }
17
18 static [Symbol.hasInstance](value) {
19 return typeof value === 'string' && value.length > 0;
20 }
21}
22
23console.log('hello' instanceof Validator); // true
24console.log('' instanceof Validator); // falseSymbol.species#
1// Control constructor for derived objects
2class MyArray extends Array {
3 static get [Symbol.species]() {
4 return Array; // Methods like map() return plain Array
5 }
6}
7
8const myArr = new MyArray(1, 2, 3);
9const mapped = myArr.map((x) => x * 2);
10
11console.log(myArr instanceof MyArray); // true
12console.log(mapped instanceof MyArray); // false
13console.log(mapped instanceof Array); // trueSymbol.isConcatSpreadable#
1// Control Array.concat() behavior
2const arr = [1, 2];
3const spreadable = {
4 0: 3,
5 1: 4,
6 length: 2,
7 [Symbol.isConcatSpreadable]: true,
8};
9
10console.log(arr.concat(spreadable)); // [1, 2, 3, 4]
11
12// Prevent array spreading
13const noSpread = [5, 6];
14noSpread[Symbol.isConcatSpreadable] = false;
15
16console.log(arr.concat(noSpread)); // [1, 2, [5, 6]]Symbol.match/replace/search/split#
1// Custom string matching
2class StartsWithMatcher {
3 constructor(prefix) {
4 this.prefix = prefix;
5 }
6
7 [Symbol.match](string) {
8 return string.startsWith(this.prefix);
9 }
10
11 [Symbol.replace](string, replacement) {
12 if (string.startsWith(this.prefix)) {
13 return replacement + string.slice(this.prefix.length);
14 }
15 return string;
16 }
17}
18
19const matcher = new StartsWithMatcher('Hello');
20
21console.log('Hello World'.match(matcher)); // true
22console.log('Hello World'.replace(matcher, 'Hi')); // 'Hi World'
23console.log('Goodbye'.match(matcher)); // falsePrivate-like Properties#
1// Symbols for pseudo-private properties
2const _private = Symbol('private');
3
4class Counter {
5 constructor() {
6 this[_private] = 0;
7 }
8
9 get count() {
10 return this[_private];
11 }
12
13 increment() {
14 this[_private]++;
15 }
16}
17
18const counter = new Counter();
19counter.increment();
20console.log(counter.count); // 1
21console.log(counter[_private]); // 1 (if you have the symbol)
22console.log(Object.keys(counter)); // []Plugin/Extension Keys#
1// Use symbols for plugin properties to avoid conflicts
2const pluginData = Symbol('pluginData');
3const pluginInit = Symbol('pluginInit');
4
5function installPlugin(obj, data) {
6 obj[pluginData] = data;
7 obj[pluginInit] = true;
8}
9
10function getPluginData(obj) {
11 if (!obj[pluginInit]) {
12 throw new Error('Plugin not initialized');
13 }
14 return obj[pluginData];
15}
16
17const myObj = { name: 'Test' };
18installPlugin(myObj, { version: '1.0' });
19
20console.log(myObj.name); // 'Test'
21console.log(getPluginData(myObj)); // { version: '1.0' }
22console.log(Object.keys(myObj)); // ['name']Type Checking with Symbols#
1// Custom type identification
2const TYPE = Symbol('type');
3
4class Dog {
5 static [TYPE] = 'Dog';
6 [TYPE] = 'Dog';
7}
8
9class Cat {
10 static [TYPE] = 'Cat';
11 [TYPE] = 'Cat';
12}
13
14function getType(obj) {
15 return obj?.[TYPE] ?? 'Unknown';
16}
17
18console.log(getType(new Dog())); // 'Dog'
19console.log(getType(new Cat())); // 'Cat'
20console.log(getType({})); // 'Unknown'Best Practices#
Usage:
✓ Unique property keys
✓ Hidden metadata
✓ Well-known symbol customization
✓ Avoiding name collisions
Global Registry:
✓ Use Symbol.for() for shared symbols
✓ Use descriptive key names
✓ Namespace keys (e.g., 'myapp.feature')
✓ Check Symbol.keyFor() when needed
Well-Known Symbols:
✓ Symbol.iterator for iterables
✓ Symbol.toStringTag for debugging
✓ Symbol.toPrimitive for coercion
✓ Symbol.hasInstance for instanceof
Avoid:
✗ Symbols for true privacy (use #private)
✗ Overusing symbols
✗ Forgetting symbols are not enumerable
✗ Converting symbols to strings accidentally
Conclusion#
Symbols provide unique identifiers for object properties and enable metaprogramming through well-known symbols. Use them for hidden properties, avoiding naming collisions in libraries, and customizing object behavior like iteration and type coercion. The global registry (Symbol.for) enables cross-realm symbol sharing. While not truly private, symbols are hidden from most iteration methods, making them useful for metadata and internal properties.