Technology · JavaScript

Advanced

JavaScript Symbols

Use unique Symbol values as collision-free object keys and customize built-in object behavior.

TL;DR
  1. 01Every Symbol() call creates a guaranteed-unique primitive value.
  2. 02Symbols make non-colliding object keys hidden from normal enumeration.
  3. 03Well-known symbols let objects customize core language behavior.

What Symbols Are

  • Symbol() creates a new primitive value that is guaranteed unique, even with an identical description.
    const a = Symbol('id');
    const b = Symbol('id');
    a === b; // false
    
  • Symbols are a primitive type alongside string, number, boolean, undefined, null, and bigint.
  • The optional description passed to Symbol() is only for debugging and doesn't affect identity.
    const s = Symbol('debug label');
    s.toString(); // "Symbol(debug label)"
    s.description; // "debug label"
    
  • Calling Symbol() with new throws a TypeError, since symbols aren't constructible.
  • Each symbol exists as its own independent value, with no relationship to symbols of the same description.
  • Typeof a symbol returns the string "symbol".

Symbols as Object Keys

  • Use a symbol as a property key to guarantee it never collides with any other key.
    const ID = Symbol('id');
    const user = { name: 'Ada', [ID]: 42 };
    user[ID]; // 42
    
  • Access a symbol-keyed property with bracket notation, since dot notation can't reference a variable.
  • Symbol keys are useful for adding metadata to objects without risking a clash with existing or future string keys.
    const TAG = Symbol('internal-tag');
    function tag(obj, value) {
      obj[TAG] = value;
      return obj;
    }
    
  • Symbol-keyed properties still show up in Object.getOwnPropertyDescriptors() and Reflect.ownKeys().
    Reflect.ownKeys(user); // ['name', Symbol(id)]
    
  • Symbols don't make a property truly private — anyone holding the symbol reference can read it.
  • Mixing symbol and string keys on the same object is common for separating public data from internal flags.

Symbols Are Skipped By Enumeration

  • for...in and Object.keys() ignore symbol-keyed properties entirely.
    const obj = { name: 'Ada', [Symbol('id')]: 1 };
    Object.keys(obj); // ['name']
    for (const key in obj) console.log(key); // logs 'name' only
    
  • JSON.stringify() also drops symbol-keyed properties from the resulting JSON string.
    JSON.stringify(obj); // '{"name":"Ada"}'
    
  • This makes symbols a safe place for metadata that shouldn't leak into serialized output or logs.
  • Use Object.getOwnPropertySymbols() when you explicitly need to list an object's symbol keys.
    Object.getOwnPropertySymbols(obj); // [Symbol(id)]
    
  • Object spread ({...obj}) does copy symbol-keyed own properties, unlike JSON.stringify.
  • Object.assign() also copies enumerable symbol properties between objects.

Well-Known Symbols

  • Well-known symbols are built-in symbols that let objects hook into core language behavior.
    const range = {
      from: 1,
      to: 3,
      [Symbol.iterator]() {
        let cur = this.from;
        const last = this.to;
        return { next: () => cur <= last ? { value: cur++, done: false } : { done: true } };
      }
    };
    [...range]; // [1, 2, 3]
    
  • Symbol.iterator makes an object work with for...of, spread syntax, and destructuring.
  • Symbol.asyncIterator does the same for for await...of loops over asynchronous data sources.
    const asyncRange = {
      async *[Symbol.asyncIterator]() {
        yield 1;
        yield 2;
      }
    };
    
  • Symbol.toPrimitive controls how an object converts to a number, string, or default value.
    const money = {
      amount: 50,
      [Symbol.toPrimitive](hint) {
        return hint === 'string' ? `$${this.amount}` : this.amount;
      }
    };
    `${money}`; // "$50"
    money + 10; // 60
    
  • Symbol.hasInstance customizes how instanceof checks behave for a class or object.
    class Even {
      static [Symbol.hasInstance](num) {
        return Number.isInteger(num) && num % 2 === 0;
      }
    }
    4 instanceof Even; // true
    

The Global Symbol Registry

  • Symbol.for(key) looks up a symbol in a global registry, creating one if it doesn't exist yet.
    const a = Symbol.for('app.id');
    const b = Symbol.for('app.id');
    a === b; // true
    
  • Unlike Symbol(), calling Symbol.for() with the same key always returns the same shared symbol.
  • The registry is shared across realms, like the main page and an iframe, in browser environments.
  • Symbol.keyFor(sym) returns the registry key for a given registered symbol, or undefined if unregistered.
    Symbol.keyFor(a); // "app.id"
    Symbol.keyFor(Symbol('local')); // undefined
    
  • Use the global registry when independent modules need to coordinate on the exact same symbol value.
  • Prefer plain Symbol() for most cases — reach for the registry only when cross-module sharing is required.
Tips
  1. 01Use a symbol property when you need a key that won't collide with other keys sharing a description.
  2. 02Reach for Symbol.for() when multiple modules or realms need to share the exact same symbol value reliably.
Warnings
  1. 01Symbol() is never called with new, since attempting that throws a TypeError because symbols aren't constructible objects.
  2. 02Symbol-keyed properties are skipped by JSON.stringify, for...in, and Object.keys, so they vanish from typical serialization.
  3. 03Two symbols created with the same description are still completely different values and never compare as equal.
FAQ
JavaScript Proxy and ReflectJavaScript Timers