Learning Objectives

  • Understand Symbol primitive type
  • Create unique identifiers
  • Use well-known symbols
  • Implement private properties
  • Apply symbol best practices

What are Symbols?

Symbols are a primitive data type introduced in ES6 for creating unique identifiers. Every symbol is guaranteed to be unique, making them perfect for property keys that won't collide with other properties.

Creating Symbols

// Create unique symbol
const id = Symbol();
const id2 = Symbol();

console.log(id === id2); // false - each is unique

// With description (for debugging)
const userId = Symbol('user id');
console.log(userId.toString()); // 'Symbol(user id)'
console.log(userId.description); // 'user id'

// Use as object property
const user = {
    name: 'John',
    [userId]: 123
};

console.log(user[userId]); // 123
console.log(user.name); // 'John'

Private Properties

const _balance = Symbol('balance');
const _transactions = Symbol('transactions');

class BankAccount {
    constructor(initialBalance) {
        this[_balance] = initialBalance;
        this[_transactions] = [];
    }
    
    deposit(amount) {
        this[_balance] += amount;
        this[_transactions].push({ type: 'deposit', amount });
    }
    
    getBalance() {
        return this[_balance];
    }
    
    getTransactions() {
        return [...this[_transactions]];
    }
}

const account = new BankAccount(1000);
account.deposit(500);

console.log(account.getBalance()); // 1500
console.log(account[_balance]); // undefined (not accessible)
console.log(Object.keys(account)); // [] (symbols not enumerable)

Well-Known Symbols

Symbol.iterator

const collection = {
    items: ['a', 'b', 'c'],
    [Symbol.iterator]() {
        let index = 0;
        return {
            next: () => ({
                value: this.items[index++],
                done: index > this.items.length
            })
        };
    }
};

// Now iterable
for (const item of collection) {
    console.log(item); // 'a', 'b', 'c'
}

// Works with spread
console.log([...collection]); // ['a', 'b', 'c']

Symbol.toStringTag

class MyClass {
    get [Symbol.toStringTag]() {
        return 'MyClass';
    }
}

const obj = new MyClass();
console.log(Object.prototype.toString.call(obj)); // [object MyClass]
console.log(obj.toString()); // [object MyClass]

Symbol.hasInstance

class MyArray {
    static [Symbol.hasInstance](instance) {
        return Array.isArray(instance);
    }
}

console.log([] instanceof MyArray); // true
console.log({} instanceof MyArray); // false

Global Symbol Registry

// Create/retrieve global symbol
const globalSym = Symbol.for('app.id');
const sameSym = Symbol.for('app.id');

console.log(globalSym === sameSym); // true

// Get key for symbol
console.log(Symbol.keyFor(globalSym)); // 'app.id'

// Regular symbols not in registry
const localSym = Symbol('local');
console.log(Symbol.keyFor(localSym)); // undefined

Accessing Symbol Properties

const sym1 = Symbol('sym1');
const sym2 = Symbol('sym2');

const obj = {
    name: 'John',
    [sym1]: 'value1',
    [sym2]: 'value2'
};

// Symbols not in Object.keys()
console.log(Object.keys(obj)); // ['name']

// Get symbol properties
console.log(Object.getOwnPropertySymbols(obj)); // [Symbol(sym1), Symbol(sym2)]

// Get all property keys
console.log(Reflect.ownKeys(obj)); // ['name', Symbol(sym1), Symbol(sym2)]

Best Practices

1. Use for truly unique identifiers
const ID = Symbol('id');
const user = { [ID]: 123, name: 'John' };
2. Implement private properties
const _private = Symbol('private');
class MyClass {
    constructor() {
        this[_private] = 'secret';
    }
}
3. Use Symbol.for() for cross-realm symbols
const shared = Symbol.for('shared.key');

Key Takeaways