Symbols

Symbol primitive type, well-known symbols, and use cases

What are Symbols?

Symbol is a primitive type introduced in ES6 that represents a unique identifier. Unlike strings, every Symbol is guaranteed to be unique, making them perfect for property keys that won't collide with other properties.

Key Characteristics

  • Unique — Every Symbol() call creates a unique symbol
  • Immutable — Cannot be changed once created
  • Not enumerable — Hidden from for...in and Object.keys()
  • Description — Optional string for debugging purposes

Creating Symbols

// Create a symbol
const sym1 = Symbol();
const sym2 = Symbol();

console.log(sym1 === sym2); // false — always unique

// Symbol with description (for debugging)
const id = Symbol("id");
console.log(id.toString());    // "Symbol(id)"
console.log(id.description);   // "id"

// Same description, still different symbols
const a = Symbol("name");
const b = Symbol("name");
console.log(a === b); // false

// ⚠️ Cannot use 'new'
new Symbol(); // TypeError: Symbol is not a constructor

Symbols as Property Keys

const ID = Symbol("id");
const SECRET = Symbol("secret");

const user = {
  name: "Alice",
  [ID]: 12345,
  [SECRET]: "password123"
};

// Access with symbol
console.log(user[ID]);     // 12345
console.log(user[SECRET]); // "password123"

// Symbol properties are hidden from normal enumeration
console.log(Object.keys(user));        // ["name"]
console.log(JSON.stringify(user));     // {"name":"Alice"}

for (const key in user) {
  console.log(key);  // Only "name"
}

// But can be accessed with:
console.log(Object.getOwnPropertySymbols(user)); // [Symbol(id), Symbol(secret)]
console.log(Reflect.ownKeys(user)); // ["name", Symbol(id), Symbol(secret)]

Symbol.for() - Global Registry

// Create/retrieve from global symbol registry
const globalSym1 = Symbol.for("app.id");
const globalSym2 = Symbol.for("app.id");

console.log(globalSym1 === globalSym2); // true — same symbol!

// Get the key for a global symbol
console.log(Symbol.keyFor(globalSym1)); // "app.id"

// Regular symbols aren't in the registry
const localSym = Symbol("local");
console.log(Symbol.keyFor(localSym)); // undefined

// Use case: Share symbols across modules/iframes
// In module A:
const SHARED_KEY = Symbol.for("myApp.sharedKey");

// In module B:
const key = Symbol.for("myApp.sharedKey");
// key === SHARED_KEY

Well-Known Symbols

JavaScript has built-in symbols that customize object behavior:

// Symbol.iterator - Make objects iterable
const range = {
  start: 1,
  end: 5,
  
  [Symbol.iterator]() {
    let current = this.start;
    const end = this.end;
    
    return {
      next() {
        if (current <= end) {
          return { value: current++, done: false };
        }
        return { done: true };
      }
    };
  }
};

console.log([...range]); // [1, 2, 3, 4, 5]

// Symbol.toStringTag - Customize Object.prototype.toString
class MyClass {
  get [Symbol.toStringTag]() {
    return "MyClass";
  }
}

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

// Symbol.toPrimitive - Control type conversion
const money = {
  amount: 100,
  currency: "USD",
  
  [Symbol.toPrimitive](hint) {
    if (hint === "number") return this.amount;
    if (hint === "string") return `${this.amount} ${this.currency}`;
    return this.amount; // default
  }
};

console.log(+money);        // 100
console.log(`${money}`);    // "100 USD"
console.log(money + 50);    // 150

More Well-Known Symbols

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

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

// Symbol.species - Constructor for derived objects
class MyArray extends Array {
  static get [Symbol.species]() {
    return Array; // map(), filter() return regular Arrays
  }
}

const arr = new MyArray(1, 2, 3);
const mapped = arr.map(x => x * 2);

console.log(mapped instanceof MyArray); // false
console.log(mapped instanceof Array);   // true

// Symbol.isConcatSpreadable
const arr1 = [1, 2];
const notSpreadable = {
  0: 3,
  1: 4,
  length: 2,
  [Symbol.isConcatSpreadable]: false
};

console.log(arr1.concat(notSpreadable)); // [1, 2, {0: 3, 1: 4, ...}]

notSpreadable[Symbol.isConcatSpreadable] = true;
console.log(arr1.concat(notSpreadable)); // [1, 2, 3, 4]

Practical Use Cases

// 1. Private-like properties (before #private syntax)
const _balance = Symbol("balance");

class BankAccount {
  constructor(initial) {
    this[_balance] = initial;
  }
  
  deposit(amount) {
    this[_balance] += amount;
  }
  
  getBalance() {
    return this[_balance];
  }
}

const account = new BankAccount(100);
console.log(account.getBalance()); // 100
console.log(account._balance);     // undefined
console.log(Object.keys(account)); // []

// 2. Avoid property collision in mixins
const flyable = (() => {
  const FLY_SPEED = Symbol("flySpeed");
  
  return {
    [FLY_SPEED]: 100,
    fly() {
      console.log(`Flying at ${this[FLY_SPEED]} mph`);
    }
  };
})();

// 3. Type branding
const StringType = Symbol("String");
const NumberType = Symbol("Number");

function branded(value, type) {
  return { value, [type]: true };
}


  
const str = branded("hello", StringType);
console.log(str[StringType]); // true
console.log(str[NumberType]); // undefined

💡 Key Takeaways

  • • Symbol() creates unique, immutable identifiers
  • • Use symbols for property keys to avoid collisions
  • • Symbol.for() creates shared global symbols
  • • Well-known symbols (Symbol.iterator, etc.) customize object behavior
  • • Symbol properties are hidden from normal enumeration
  • • Great for libraries and frameworks to avoid conflicts