Back to Blog
JavaScriptSymbolPrimitivesMetaprogramming

JavaScript Symbol Guide

Master JavaScript Symbols for unique identifiers, well-known symbols, and metaprogramming.

B
Bootspring Team
Engineering
May 23, 2019
5 min read

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); // false

Symbol.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); // true

Symbol.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)); // false

Private-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.

Share this article

Help spread the word about Bootspring