Well-Known Symbols

JavaScript has built-in Symbols that customize object behavior. These are called well-known Symbols. They allow you to hook into language mechanics like iteration, type conversion, and string representation.

Custom Iteration with Symbol.iterator

Makes an object iterable with for...of:

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 };
      },
    };
  },
};

for (const num of range) {
  console.log(num);  // 1, 2, 3, 4, 5
}

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

Customizing String Tags with Symbol.toStringTag

Customizes the Object.prototype.toString() result:

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

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

Controlling Type Conversion with Symbol.toPrimitive

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 (number hint)
console.log(`${money}`);  // "100 USD" (string hint)
console.log(money + 50);  // 150 (default hint)

Other Well-Known Symbols

Symbol Purpose
Symbol.hasInstance Customize instanceof behavior
Symbol.isConcatSpreadable Control array spreading in concat()
Symbol.match Make object work with String.match()
Symbol.replace Make object work with String.replace()
Symbol.search Make object work with String.search()
Symbol.split Make object work with String.split()
Symbol.species Constructor for derived objects
Symbol.unscopables Exclude properties from with binding

Practical Use Cases

Avoiding Property Name Collisions

When adding properties to objects you do not control:

// Library adds metadata without risking collision
const metadata = Symbol("metadata");

function addMetadata(obj, data) {
  obj[metadata] = data;
}

function getMetadata(obj) {
  return obj[metadata];
}

const userObj = { name: "Bob" };
addMetadata(userObj, { timestamp: Date.now() });

// User's properties are safe
console.log(Object.keys(userObj));  // ["name"]
console.log(getMetadata(userObj));  // { timestamp: ... }

Defining Constants

Symbols guarantee unique values for constants:

const Status = {
  PENDING: Symbol("pending"),
  APPROVED: Symbol("approved"),
  REJECTED: Symbol("rejected"),
};

function processRequest(status) {
  switch (status) {
    case Status.PENDING:
      return "Waiting...";
    case Status.APPROVED:
      return "Done!";
    case Status.REJECTED:
      return "Failed";
  }
}

// Can't accidentally match with a string
console.log(processRequest("pending"));  // undefined
console.log(processRequest(Status.PENDING));  // "Waiting..."

Private-ish Properties

This is not real privacy. But a property keyed by a Symbol is not reachable from code that does not have a reference to that same Symbol:

const _count = Symbol("count");

class Counter {
  constructor() {
    this[_count] = 0;
  }

  increment() {
    this[_count]++;
  }

  get value() {
    return this[_count];
  }
}

const counter = new Counter();
counter.increment();
console.log(counter.value);  // 1
console.log(counter._count);  // undefined (not accessible by string)

Summary

  • Symbols are unique, immutable primitive values
  • Use them for property keys that will not collide with other keys
  • Symbol properties are hidden from normal enumeration
  • Well-known Symbols customize JavaScript’s built-in behaviors
  • The global registry (Symbol.for()) allows sharing Symbols across code