Symbols & Well-Known OOP Protocols

advanced Symbol Symbol.iterator Symbol.toPrimitive Symbol.hasInstance iterable

Symbols are a primitive type added in ES6. Each Symbol is guaranteed to be unique — even if two Symbols have the same description. But the real OOP power comes from well-known Symbols — special built-in Symbols that let us hook into JavaScript’s internal behavior.

What Is a Symbol?

A Symbol is a unique, immutable identifier. Unlike strings, two Symbols are never equal.

const a = Symbol("id");
const b = Symbol("id");
console.log(a === b); // false — each Symbol is unique

// We use Symbols as property keys
const user = { [a]: 123 };
console.log(user[a]); // 123

Symbols don’t show up in for...in loops or Object.keys(), making them perfect for “hidden” metadata on objects.

Why Well-Known Symbols Matter for OOP

JavaScript has a set of built-in Symbols (like Symbol.iterator, Symbol.toPrimitive) that act as hooks. When we define methods with these Symbol keys on our objects, we’re telling JavaScript how our object should behave with built-in operations like for...of, string conversion, and instanceof.

In simple language, well-known Symbols let our custom objects “speak JavaScript’s language.”

Symbol.iterator — Making Objects Iterable

When we use for...of on an array, JavaScript calls array[Symbol.iterator]() behind the scenes. We can define this on our own objects to make them work with for...of, spread syntax, and destructuring.

An iterator is an object with a next() method that returns { value, done }.

class Range {
  constructor(start, end) {
    this.start = start;
    this.end = end;
  }
  [Symbol.iterator]() {
    let current = this.start;
    const end = this.end;
    return {
      next() {
        return current <= end
          ? { value: current++, done: false }
          : { done: true };
      }
    };
  }
}

const range = new Range(1, 5);
console.log([...range]);       // [1, 2, 3, 4, 5]
for (const n of range) { }     // works!
const [a, b] = new Range(10, 20); // a = 10, b = 11

Generators as a Shortcut

Writing iterators manually is verbose. Generators make it much cleaner. A generator function (with function*) automatically returns an iterator.

class Fibonacci {
  [Symbol.iterator]() {
    return this.generate();
  }
  *generate() {
    let [a, b] = [0, 1];
    while (true) {
      yield a;       // pauses here, returns value
      [a, b] = [b, a + b];
    }
  }
}

const first10 = [...new Fibonacci()].slice(0, 10); // won't work — infinite!

// Instead, take manually:
const fib = new Fibonacci();
const iter = fib[Symbol.iterator]();
for (let i = 0; i < 10; i++) {
  console.log(iter.next().value); // 0, 1, 1, 2, 3, 5, 8, 13, 21, 34
}

We can also use *[Symbol.iterator]() directly in a class to combine both in one:

class EvenNumbers {
  constructor(limit) { this.limit = limit; }
  *[Symbol.iterator]() {
    for (let i = 0; i <= this.limit; i += 2) yield i;
  }
}

console.log([...new EvenNumbers(10)]); // [0, 2, 4, 6, 8, 10]

Symbol.toPrimitive — Controlling Type Conversion

When JavaScript needs to convert our object to a primitive (number, string, or default), it calls [Symbol.toPrimitive](hint). The hint tells us what type is expected.

class Money {
  constructor(amount, currency) {
    this.amount = amount;
    this.currency = currency;
  }
  [Symbol.toPrimitive](hint) {
    if (hint === "number") return this.amount;
    if (hint === "string") return `${this.amount} ${this.currency}`;
    return this.amount; // default
  }
}

const price = new Money(42, "USD");
console.log(`Price: ${price}`);  // "Price: 42 USD" (hint = string)
console.log(+price);             // 42 (hint = number)
console.log(price + 10);         // 52 (hint = default)

Without this, JavaScript would just give us "[object Object]" for string conversion. Now our object converts meaningfully.

Symbol.hasInstance — Customizing instanceof

instanceof calls [Symbol.hasInstance] on the right-hand side. We can override it to change what instanceof means for our class.

class EvenNumber {
  static [Symbol.hasInstance](num) {
    return typeof num === "number" && num % 2 === 0;
  }
}

console.log(4 instanceof EvenNumber);   // true
console.log(7 instanceof EvenNumber);   // false
console.log("hi" instanceof EvenNumber); // false

This is unusual — most of the time we don’t need to override instanceof. But it’s good to know it’s possible.

Symbol.species — Controlling Derived Constructors

When we extend Array or Promise, methods like .map() and .then() need to create new instances. Symbol.species tells them which constructor to use.

class FilteredArray extends Array {
  static get [Symbol.species]() {
    return Array; // .map() returns a plain Array, not FilteredArray
  }
}

const filtered = new FilteredArray(1, 2, 3);
const doubled = filtered.map(x => x * 2);

console.log(doubled instanceof FilteredArray); // false
console.log(doubled instanceof Array);         // true

This is handy when our subclass has expensive constructors or special initialization that shouldn’t run for every intermediate operation.

Symbol.toStringTag

When we call Object.prototype.toString.call(something), it returns "[object Type]". We can control Type with Symbol.toStringTag.

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

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

This is useful for debugging — we can give our custom classes meaningful type tags instead of the generic "[object Object]".

Quick Reference

SymbolWhat It ControlsTriggered By
Symbol.iteratorIteration protocolfor...of, spread, destructuring
Symbol.toPrimitiveType conversion+obj, ${obj}, comparisons
Symbol.hasInstanceinstanceof checkx instanceof MyClass
Symbol.speciesDerived constructors.map(), .filter(), .then()
Symbol.toStringTagString representationObject.prototype.toString.call()

In simple language, well-known Symbols are hooks that let our custom objects plug into JavaScript’s built-in operations. The most practically useful one is Symbol.iterator — it lets any object work with for...of and spread syntax, which comes up all the time.

References