JavaScript Symbol Type: Complete Guide

Symbol is a unique, immutable primitive type in JavaScript. Learn what symbols are, why they exist, and how to use them for private property keys, well-known symbols, and metaprogramming.

6 min read

A Symbol is a primitive value that is guaranteed to be unique. No two symbols are ever equal, even if they have the same description:

javascriptjavascript
const a = Symbol("id");
const b = Symbol("id");
 
console.log(a === b); // false -- every symbol is unique
console.log(a.description); // "id"

The description is a label for debugging. It does not affect the symbol's identity. Think of it as a comment attached to an otherwise anonymous value.

Symbols are the seventh and most recent primitive type in JavaScript, joining string, number, bigint, boolean, undefined, and null. They were added in ES2015 (ES6).

Why Symbols Exist

Before symbols, every object property key was a string. This created two problems:

  • Name collisions. If a library added a property called id to objects, and your code also used id, one would overwrite the other.
  • No private-by-convention mechanism. Prefixing with an underscore was a convention, not enforcement.

Symbols solve both. A symbol-keyed property can never collide with a string-keyed property or with another symbol.

Using Symbols as Property Keys

You use symbols as object keys with bracket notation:

javascriptjavascript
const userId = Symbol("id");
const userName = Symbol("name");
 
const user = {
  [userId]: 42,
  [userName]: "Alex",
  name: "Alex" // This is a regular string key -- separate from the symbol
};
 
console.log(user[userId]);   // 42
console.log(user.name);      // "Alex" -- the string key
console.log(user[userName]); // "Alex" -- the symbol key, same value, different key

The object now has three properties: a string-keyed "name" and two symbol-keyed properties. They never interfere with each other.

Symbol-keyed properties are hidden from most enumeration:

javascriptjavascript
console.log(Object.keys(user));          // ["name"] -- only string keys
console.log(Object.getOwnPropertyNames(user)); // ["name"]
console.log(Object.getOwnPropertySymbols(user)); // [Symbol(id), Symbol(name)]
 
// for...in also skips symbols
for (const key in user) {
  console.log(key); // "name" only
}

This makes symbols ideal for attaching metadata or internal state to objects without cluttering the public interface.

Well-Known Symbols

JavaScript defines a set of built-in symbols that let you customize how objects behave with language features. These are called well-known symbols:

SymbolCustomizes
Symbol.iteratorHow the object works with for...of and spread
Symbol.asyncIteratorHow the object works with for await...of
Symbol.toPrimitiveHow the object converts to a primitive (for addition, String conversion, etc.)
Symbol.toStringTagWhat Object.prototype.toString() returns
Symbol.hasInstanceHow instanceof decides if an object is an instance
Symbol.speciesWhich constructor to use for derived objects
Symbol.isConcatSpreadableWhether the object spreads in Array.prototype.concat()
Symbol.match / Symbol.replace / Symbol.search / Symbol.splitCustom regex-like behavior for string methods

Symbol.iterator -- Making Objects Iterable

The most commonly used well-known symbol is Symbol.iterator. Define it on any object to make it work with for...of:

javascriptjavascript
const range = {
  from: 1,
  to: 5,
 
  [Symbol.iterator]() {
    let current = this.from;
    const last = this.to;
 
    return {
      next() {
        if (current <= last) {
          return { value: current++, done: false };
        }
        return { value: undefined, done: true };
      }
    };
  }
};
 
for (const num of range) {
  console.log(num); // 1, 2, 3, 4, 5
}
 
console.log([...range]); // [1, 2, 3, 4, 5]

The Symbol.iterator method must return an object with a next() method, and that method must return a value/done object. This is the iterator protocol, covered in full in /javascript/javascript-iterators-complete-guide.

Generators, covered in /javascript/javascript-generators-deep-dive-full-guide, are a concise way to build iterator functions.

Symbol.toPrimitive -- Controlling Type Coercion

When JavaScript needs to convert an object to a primitive (for example, when you use addition or String conversion), it calls Symbol.toPrimitive:

javascriptjavascript
const money = {
  amount: 49.99,
  currency: "USD",
 
  [Symbol.toPrimitive](hint) {
    if (hint === "number") return this.amount;
    if (hint === "string") return `${this.currency} ${this.amount}`;
    return this.amount; // default
  }
};
 
console.log(+money);        // 49.99 (number hint)
console.log(`${money}`);    // "USD 49.99" (string hint)
console.log(money + 10);    // 59.99 (default hint → number)

The hint parameter tells you the context: "number" for math operations, "string" for string interpolation, and "default" when the engine is unsure.

Symbol.toStringTag -- Custom toString Output

By default, Object.prototype.toString.call(obj) returns "[object Object]". Override Symbol.toStringTag to return a custom label:

javascriptjavascript
class ApiResponse {
  get [Symbol.toStringTag]() {
    return "ApiResponse";
  }
}
 
const res = new ApiResponse();
console.log(Object.prototype.toString.call(res)); // "[object ApiResponse]"

This is used by libraries and frameworks that check the type tag for duck-typing. It does not affect the typeof operator.

The Global Symbol Registry

Symbol.for() looks up or creates a symbol in a global registry that spans all realms, such as iframes and web workers:

javascriptjavascript
const shared1 = Symbol.for("app.config");
const shared2 = Symbol.for("app.config");
 
console.log(shared1 === shared2); // true -- same symbol from registry
 
// Get the key back
console.log(Symbol.keyFor(shared1)); // "app.config"
 
// Symbols created directly are not in the registry
const local = Symbol("app.config");
console.log(Symbol.keyFor(local)); // undefined

Use Symbol.for() when you need the same symbol value across different execution contexts, such as an iframe and its parent page, or a shared library module. In normal application code, plain Symbol() is almost always sufficient and safer.

Practical Use Cases

Enums with guaranteed uniqueness

javascriptjavascript
const Status = {
  IDLE: Symbol("idle"),
  LOADING: Symbol("loading"),
  SUCCESS: Symbol("success"),
  ERROR: Symbol("error")
};
 
function handleStatus(status) {
  switch (status) {
    case Status.LOADING:
      return "Loading...";
    case Status.SUCCESS:
      return "Done!";
    default:
      return "Unknown";
  }
}
 
console.log(handleStatus(Status.SUCCESS)); // "Done!"

String enums like Status.LOADING = "loading" work too, but symbols guarantee that no external input or string value can accidentally match. Status.LOADING will never equal any string, ever.

Hiding internal state in a class

javascriptjavascript
const _counter = Symbol("counter");
 
class ClickTracker {
  constructor() {
    this[_counter] = 0;
  }
 
  click() {
    this[_counter] += 1;
    console.log(`Clicked ${this[_counter]} times`);
  }
}
 
const tracker = new ClickTracker();
tracker.click(); // Clicked 1 times
tracker.click(); // Clicked 2 times
 
// External code cannot accidentally access or overwrite _counter
console.log(Object.keys(tracker)); // [] -- no public counter property

Note: symbols do not provide true privacy. Object.getOwnPropertySymbols(tracker) still reveals them, so symbols provide collision-avoidance, not access control.

For true private fields, use the # private field syntax added in ES2022.

Symbols vs Strings vs Private Fields

FeatureString keysSymbol keysPrivate fields
Enumerable?YesNoNo
Collision with other code?PossibleImpossibleImpossible
Accessible via reflection?YesYes (getOwnPropertySymbols)No
In JSON?YesNoNo
Accessible from outside the class?YesYesNo
Syntaxobj.key or obj["key"]obj[symbolVar]this.#field

Symbols sit between string keys (fully public) and private fields (fully inaccessible). Use strings for public API properties, symbols for collision-free metadata, and private fields for true encapsulation.

Rune AI

Rune AI

Key Insights

  • Symbol is a primitive type whose values are always unique, even with the same description.
  • Symbols are used as property keys that cannot collide with string keys or other symbols.
  • Well-known symbols (Symbol.iterator, Symbol.toPrimitive, etc.) define built-in object behaviors.
  • Symbol.for() and Symbol.keyFor() provide a global symbol registry for cross-realm sharing.
  • Symbols are not enumerable in for...in and are ignored by JSON.stringify.
RunePowered by Rune AI

Frequently Asked Questions

When should I use a Symbol instead of a string key?

Use a Symbol when you need a property key that is guaranteed not to collide with any other property, including properties added by libraries, future language features, or other parts of your code. Symbols are also the standard way to implement well-known protocols like iterators.

What is the difference between Symbol() and Symbol.for()?

Symbol() creates a unique symbol every time, even if called with the same description. Symbol.for() checks a global registry and returns an existing symbol if one with the same key already exists. Use Symbol.for() when you need the same symbol across different realms or modules.

Conclusion

Symbol is JavaScript's seventh primitive type, designed for use cases where uniqueness and non-collision matter more than discoverability. They are the key mechanism behind JavaScript's iteration, string conversion, and species protocols. While everyday application code rarely creates symbols directly, understanding them explains how for...of loops, spread operators, and custom object behaviors actually work.