Advanced JavaScript (ES6+)

Symbols - Unique and Immutable Identifiers

13 min Lesson 23 of 40

Symbols - Unique and Immutable Identifiers

ES6 introduced Symbols, a new primitive data type that creates unique, immutable identifiers. Symbols are primarily used for creating private object properties, defining well-known behaviors, and avoiding property name collisions in objects.

What are Symbols?

A Symbol is a unique and immutable primitive value that can be used as an object property key. Key characteristics:

  • Every Symbol is completely unique, even with the same description
  • Symbols are immutable (cannot be changed once created)
  • Symbols can be used as object property keys
  • Symbol properties don't appear in for...in loops or Object.keys()
  • Symbols enable meta-programming through well-known symbols
Key Fact: JavaScript now has seven primitive types: string, number, boolean, null, undefined, bigint, and symbol.

Creating Symbols

You create Symbols using the Symbol() function (not a constructor):

// Basic symbol creation const sym1 = Symbol(); const sym2 = Symbol(); console.log(sym1 === sym2); // false - Each Symbol is unique! // Symbols with descriptions (for debugging) const id = Symbol('id'); const userId = Symbol('userId'); const debugId = Symbol('debugId'); console.log(id.toString()); // 'Symbol(id)' console.log(id.description); // 'id' (ES2019+) // Symbols are always unique, even with same description const a = Symbol('mySymbol'); const b = Symbol('mySymbol'); console.log(a === b); // false // Symbol cannot be used with 'new' keyword // const wrong = new Symbol(); // TypeError!
Important: Unlike strings or numbers, Symbols cannot be implicitly converted to strings. You must use .toString() or .description explicitly.

Using Symbols as Object Keys

Symbols are perfect for creating properties that won't conflict with existing or future properties:

const id = Symbol('id'); const user = { name: 'Alice', age: 25, [id]: 12345 // Symbol as computed property }; console.log(user[id]); // 12345 console.log(user.name); // 'Alice' // Symbol properties are hidden from normal enumeration console.log(Object.keys(user)); // ['name', 'age'] - no Symbol! console.log(Object.values(user)); // ['Alice', 25] - no Symbol value! for (let key in user) { console.log(key); // Only 'name' and 'age', not Symbol } // But Symbols are not completely hidden console.log(Object.getOwnPropertySymbols(user)); // [Symbol(id)] console.log(Reflect.ownKeys(user)); // ['name', 'age', Symbol(id)]

Global Symbol Registry

Sometimes you need to share Symbols across different parts of your application. The global Symbol registry helps with this:

// Symbol.for() - Create or retrieve from global registry const globalSym1 = Symbol.for('app.id'); const globalSym2 = Symbol.for('app.id'); console.log(globalSym1 === globalSym2); // true - Same Symbol! // Symbol.keyFor() - Get the key from global registry console.log(Symbol.keyFor(globalSym1)); // 'app.id' // Local symbols are not in global registry const localSym = Symbol('local'); console.log(Symbol.keyFor(localSym)); // undefined // Real-world example: Shared constants // In file1.js const STATUS = Symbol.for('app.status'); // In file2.js const STATUS = Symbol.for('app.status'); // Same Symbol!
Best Practice: Use Symbol.for() when you need to share Symbols across files or modules. Use regular Symbol() for private properties within a single module.

Well-Known Symbols

JavaScript provides built-in Symbols that allow you to customize object behavior. These are called "well-known symbols":

Common Well-Known Symbols: Symbol.iterator - Define custom iteration behavior Symbol.toStringTag - Customize Object.prototype.toString() Symbol.toPrimitive - Control type conversion Symbol.hasInstance - Customize instanceof behavior Symbol.species - Specify constructor for derived objects Symbol.match - Define string matching behavior Symbol.search - Define string search behavior Symbol.replace - Define string replace behavior Symbol.split - Define string split behavior

Symbol.iterator - Custom Iteration

The most commonly used well-known Symbol is Symbol.iterator:

// Create a custom iterable object const range = { from: 1, to: 5, [Symbol.iterator]() { let current = this.from; const last = this.to; return { next() { if (current <= last) { return { value: current++, done: false }; } else { return { done: true }; } } }; } }; // Now the object is iterable! for (let num of range) { console.log(num); // 1, 2, 3, 4, 5 } // Can use spread operator console.log([...range]); // [1, 2, 3, 4, 5] // Can use Array.from() console.log(Array.from(range)); // [1, 2, 3, 4, 5]

Symbol.toStringTag

Customize the string returned by Object.prototype.toString():

class CustomClass { constructor(name) { this.name = name; } get [Symbol.toStringTag]() { return 'CustomClass'; } } const obj = new CustomClass('test'); console.log(Object.prototype.toString.call(obj)); // [object CustomClass] // Compare with default behavior class RegularClass { constructor(name) { this.name = name; } } const regular = new RegularClass('test'); console.log(Object.prototype.toString.call(regular)); // [object Object]

Symbol.toPrimitive

Control how objects are converted to primitive values:

const temperature = { celsius: 25, [Symbol.toPrimitive](hint) { if (hint === 'number') { return this.celsius; } if (hint === 'string') { return `${this.celsius}°C`; } // default hint return this.celsius; } }; console.log(+temperature); // 25 (number hint) console.log(`${temperature}`); // '25°C' (string hint) console.log(temperature + 5); // 30 (default hint) // Another example: Custom money object const price = { amount: 100, currency: 'USD', [Symbol.toPrimitive](hint) { if (hint === 'number') { return this.amount; } return `${this.currency} ${this.amount}`; } }; console.log(+price); // 100 console.log(`Price: ${price}`); // 'Price: USD 100' console.log(price * 2); // 200

Private Properties with Symbols

Before private class fields (#private), Symbols were commonly used for privacy:

const _password = Symbol('password'); const _validatePassword = Symbol('validatePassword'); class User { constructor(username, password) { this.username = username; this[_password] = password; } [_validatePassword](input) { return input === this[_password]; } login(password) { if (this[_validatePassword](password)) { console.log('Login successful!'); return true; } console.log('Invalid password'); return false; } } const user = new User('alice', 'secret123'); console.log(user.username); // 'alice' console.log(user.password); // undefined console.log(user[_password]); // Only works if you have reference to Symbol user.login('wrong'); // Invalid password user.login('secret123'); // Login successful! // Symbol properties don't show up in console or JSON console.log(Object.keys(user)); // ['username'] console.log(JSON.stringify(user)); // {"username":"alice"}
Modern Alternative: ES2022 introduced true private fields using # prefix, which provide stronger privacy than Symbols.

Practical Examples

// Example 1: Metadata without name collision const metadata = Symbol('metadata'); function addMetadata(obj, data) { obj[metadata] = data; } const article = { title: 'ES6 Features', author: 'Alice' }; addMetadata(article, { created: new Date(), views: 0 }); console.log(article.title); // 'ES6 Features' console.log(article[metadata]); // {created: ..., views: 0} // Example 2: Private methods in object literal const privateMethod = Symbol('privateMethod'); const calculator = { [privateMethod](x, y) { return x + y; }, add(x, y) { console.log('Adding numbers...'); return this[privateMethod](x, y); } }; console.log(calculator.add(5, 3)); // Adding numbers... 8 // calculator[privateMethod] is hidden from normal access // Example 3: Preventing property conflicts in libraries const librarySymbol = Symbol.for('myLibrary.config'); // Library code function setupLibrary(element) { element[librarySymbol] = { initialized: true, version: '1.0.0' }; } // User code won't accidentally override library properties const div = document.createElement('div'); div.config = 'user config'; // Safe - won't conflict setupLibrary(div);

Symbol Methods and Properties

const sym = Symbol('mySymbol'); // Properties console.log(sym.description); // 'mySymbol' (ES2019+) // Methods console.log(sym.toString()); // 'Symbol(mySymbol)' console.log(sym.valueOf()); // Returns the Symbol itself // Static methods Symbol.for('key'); // Get/create global Symbol Symbol.keyFor(sym); // Get key for global Symbol // Getting Symbol properties const obj = { [Symbol('a')]: 1, [Symbol('b')]: 2, x: 3 }; console.log(Object.getOwnPropertySymbols(obj)); // [Symbol(a), Symbol(b)] console.log(Reflect.ownKeys(obj)); // ['x', Symbol(a), Symbol(b)]

Practice Exercise:

Challenge: Create a collection class with a private size counter using Symbols.

const _items = Symbol('items'); const _count = Symbol('count'); class Collection { constructor() { this[_items] = []; this[_count] = 0; } add(item) { this[_items].push(item); this[_count]++; } remove(item) { const index = this[_items].indexOf(item); if (index > -1) { this[_items].splice(index, 1); this[_count]--; } } get size() { return this[_count]; } [Symbol.iterator]() { return this[_items][Symbol.iterator](); } } const collection = new Collection(); collection.add('apple'); collection.add('banana'); console.log(collection.size); // 2 console.log([...collection]); // ['apple', 'banana']

Try it yourself: Add a clear() method and a has(item) method.

Summary

In this lesson, you learned:

  • Symbols are unique, immutable primitive values used as property keys
  • Every Symbol is unique, even with the same description
  • Symbol properties are hidden from normal enumeration (keys, values, for...in)
  • Symbol.for() and Symbol.keyFor() manage the global Symbol registry
  • Well-known Symbols customize object behavior (iterator, toStringTag, toPrimitive)
  • Symbols enable pseudo-private properties before # private fields
  • Symbols prevent property name collisions in objects and libraries
Next Up: In the next lesson, we'll explore Iterators and Generators - advanced iteration control!