Back to JavaScript and TypeScript

Symbols and Well-Known Symbols

Symbols are JavaScript's seventh primitive type guaranteed-unique values used as collision-free property keys. Well-known symbols go further: they let your objects participate in language mechanics like for...of, instanceof, and type coercion.

Fundamentals

Creating and Using Symbols

Every call to Symbol() produces a fresh unique value. The optional string argument is a description for debugging only it does not affect identity.

Symbols are always unique

Symbol() creates a value that is guaranteed to be unique even if you use the same description. Two calls to Symbol('id') produce two completely different values. There is no way to create a duplicate.

const a = Symbol("id")
const b = Symbol("id")

console.log(a === b)         // false  always different
console.log(typeof a)        // "symbol"
console.log(a.description)   // "id"  (read-only label)
console.log(String(a))       // "Symbol(id)"

Symbols as non-colliding property keys

Because symbols are unique, they're ideal as property keys when you want to add metadata to an object without risking a name collision with existing string keys especially on objects you don't own.

const ID  = Symbol("id")
const TAG = Symbol("tag")

const user = { name: "Alice" }
user[ID]  = 42
user[TAG] = "admin"

console.log(user[ID])   // 42
console.log(user[TAG])  // "admin"
console.log(user.id)    // undefined  no string collision

// Symbol keys are invisible to most enumeration
console.log(Object.keys(user))        // ["name"]
console.log(Object.getOwnPropertySymbols(user))  // [Symbol(id), Symbol(tag)]

Symbol.for shared global registry

Symbol.for(key) looks up (or creates) a symbol in a global registry keyed by string. Two calls with the same key return the *same* symbol useful for cross-module or cross-realm sharing.

const s1 = Symbol.for("app.token")
const s2 = Symbol.for("app.token")

console.log(s1 === s2)         // true  same symbol
console.log(Symbol.keyFor(s1)) // "app.token"

// vs regular Symbol  never shared
const local = Symbol("app.token")
console.log(local === s1)      // false

Well-Known Symbols

Well-Known Symbols

JavaScript ships with a set of built-in symbols on the Symbol object. Implement them on your classes to hook into core language behaviour.

Symbol.iterator make objects iterable

Adding a [Symbol.iterator]() method makes any object work with for...of, spread, and destructuring. This is the same protocol arrays and strings implement.

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 }
          : { value: undefined, done: true }
      }
    }
  }
}

const r = new Range(1, 5)
console.log([...r])           // [1, 2, 3, 4, 5]
for (const n of r) console.log(n)   // 1 2 3 4 5

Symbol.toPrimitive control type coercion

[Symbol.toPrimitive](hint) is called whenever JavaScript tries to convert your object to a primitive. hint is "number", "string", or "default" depending on the context.

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"  (string hint)
console.log(price * 2)          // 84               (number hint)
console.log(price + 1)          // 43               (default hint)

Symbol.hasInstance customise instanceof

[Symbol.hasInstance](value) lets a class decide what instanceof means for it. This is how you can make instanceof work on plain numbers, strings, or arbitrary conditions.

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

console.log(2  instanceof EvenNumber)  // true
console.log(3  instanceof EvenNumber)  // false
console.log(42 instanceof EvenNumber)  // true

Symbol.toStringTag customise Object.prototype.toString

[Symbol.toStringTag] controls what Object.prototype.toString.call(obj) returns. Useful for debugging and for libraries that need to identify custom types safely.

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

const q = new Queue()
console.log(Object.prototype.toString.call(q))  // "[object Queue]"

// Built-ins use this too:
console.log(Object.prototype.toString.call(new Map()))     // "[object Map]"
console.log(Object.prototype.toString.call(new Promise(() => {})))  // "[object Promise]"

Reference

Quick Reference

The most commonly implemented well-known symbols and what triggers them.

SymbolTriggered by
Symbol.iteratorfor...of, spread, destructuring
Symbol.asyncIteratorfor await...of
Symbol.toPrimitiveType coercion (+ - * template literals)
Symbol.toStringTagObject.prototype.toString.call()
Symbol.hasInstanceinstanceof
Symbol.speciesDerived class construction in map/filter/etc.
Symbol.isConcatSpreadableArray.prototype.concat