A Symbol is a primitive value that’s guaranteed to be unique. Even two symbols with the same description are not equal. That uniqueness makes them useful as non-colliding property keys.

Creating a Symbol

Create one with Symbol(). The string argument is a label for debugging only, not an identifier:

const mySymbol = Symbol('mySymbol')

Two calls with the same label produce different symbols:

Symbol('foo') === Symbol('foo') // false

Using symbols as object property keys

Symbol keys don’t show up in for...in loops or Object.keys(), so they’re invisible to code that doesn’t hold a reference to the symbol:

const mySymbol = Symbol('mySymbol')

const myObject = {
  [mySymbol]: 'Hello, World!'
}

console.log(myObject[mySymbol]) // 'Hello, World!'
console.log(Object.keys(myObject)) // []

This is the main practical use: adding internal properties to an object without polluting its visible key space. Useful when extending objects you don’t own or building library code where internal state shouldn’t be visible to users.

Using symbols for pseudo-private properties

Symbol-keyed fields won’t surface in regular iteration, so they behave like private state:

const mySymbol = Symbol('mySymbol')

class MyClass {
  [mySymbol] = 'private value'

  getPrivateValue() {
    return this[mySymbol]
  }
}

const myObject = new MyClass()
console.log(myObject.getPrivateValue()) // 'private value'
console.log(myObject[mySymbol])         // 'private value'

They’re not truly private though. Anyone with access to the symbol reference can still read the value. For actual private fields in modern JS, use the # prefix:

class MyClass {
  #value = 'private value'

  getValue() {
    return this.#value
  }
}

Symbol.for and the global registry

Symbol() always creates a fresh symbol. Symbol.for() checks a global registry first and reuses an existing symbol if one with that key already exists:

Symbol.for('shared') === Symbol.for('shared') // true
Symbol('shared') === Symbol('shared')           // false

This is useful when you need the same symbol across different modules or realms (like iframes) without explicitly passing it around. Symbol.keyFor() retrieves the key for a registry symbol:

const s = Symbol.for('app.theme')
Symbol.keyFor(s) // 'app.theme'

Well-known symbols

JavaScript itself uses symbols to expose built-in behaviors. Symbol.iterator is the most common one. Implementing it on a class makes the class iterable:

class Range {
  constructor(private start: number, private end: number) {}

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

for (const n of new Range(1, 5)) {
  console.log(n) // 1, 2, 3, 4, 5
}

Other well-known symbols: Symbol.toPrimitive (controls type coercion), Symbol.hasInstance (customizes instanceof), Symbol.asyncIterator (for for await...of).