Skip to main content

JavaScript Symbols: The Fairy Tale of Unique Identity

The Symbol Fairy Story

Once upon a time, in the magical kingdom of JavaScript, there lived many objects who shared their treasures (properties) with anyone who knew their names. But this caused chaos! Sometimes, different wizards (developers) would accidentally use the same property names, causing conflicts and overwriting each other's magical spells.

The JavaScript council decided they needed something special—a way to create truly unique keys that could never be duplicated, even if they had the same description. So they summoned the Symbol Fairy, who had the power to create mystical keys that were guaranteed to be unique across the entire kingdom.

Symbol_fairy

Symbol is some primitive data type nothing very different from other primitive data types (number, string, boolean etc..) supported by JavaScript. So you want a symbol? Just remember the following:

1 - Symbol fairy (an inbuilt JavaScript object) distributes unique symbols

2 - She has a wand (the constructor of the Symbol object)

3 - To make a wish you just say Symbol() or you can add your own description to the wish Symbol("some_key"), that doesn't make much difference. No matter how many times you make the same wish you will get a different symbol.

4 - Very important, you cannot ask for a new wand itself. No cheating! new Symbol() - Throws a typeError in JavaScript. So, its only Symbol() and not new Symbol().

5 - Symbol fairy has a secret, if you ask for your wish like this - Symbol.for("some_key"), she stores all the symbols you asked for, in the fairyland (Global Symbol Registry) and you can get the same Symbol again by calling Symbol.for("some_key") anytime. Don't worry this does not break the rule of duplicates. You just keep getting the same symbol that's stored for you in the Global Symbol Registry for some purpose.

Alright let's come back to life and discuss Symbols quickly. We now know that Symbols are nothing but unique values returned when we make a call to the constructor of the inbuilt Symbol object. Symbol('myKey') != Symbol('yourKey') and unsurprisingly Symbol('myKey') != Symbol('myKey'). Every Symbol() call is guaranteed to return a unique Symbol. We also know that by simply saying Symbol.for('myKey') we create a Symbol in the Global Registry and if it was already created earlier, you get access to the same old Symbol. Nothing complicated!


What Are Symbols?

Symbols are a primitive data type introduced in ES6 (ES2015) that represents a unique identifier. Unlike strings or numbers, every symbol is guaranteed to be unique, even if created with the same description.

const sym1 = Symbol('id');
const sym2 = Symbol('id'); // optional description 'id'
const sym3 = Symbol();
console.log(sym1 === sym2); // false - they're unique!

Symbols are immutable and cannot be changed once created. They serve as unique property keys for objects, helping prevent accidental property name collisions.


Properties of Symbols

Here are some key properties of Symbols:

1. Unique: Each Symbol is unique and cannot be duplicated.

const a = Symbol('test');
const b = Symbol('test');
console.log(a === b); // false - always unique

2. Immutable: Symbols cannot be changed or modified once created.

const sym = Symbol('immutable');
// You cannot modify a symbol once created

3. Not convertible: Symbols cannot be converted to a different type, such as a string or number.

const sym = Symbol('test');
console.log(sym + ''); // TypeError: Cannot convert a Symbol value to a string
console.log(Number(sym)); // TypeError: Cannot convert a Symbol value to a number

4. Non-enumerable by nature: Symbols are hidden from normal object enumeration.

const sym = Symbol('hidden');
const obj = {
visible: 'shown',
[sym]: 'hidden'
};

// Symbol properties don't show up in normal enumeration
console.log(Object.keys(obj)); // ['visible'] - symbol not included
for (let key in obj) {
console.log(key); // Only logs 'visible'
}

Why Creating new Symbol() Throws an Error

Symbol is a primitive data type, not an object constructor. Unlike other constructors (like new Object() or new Array()), Symbol is designed to return primitive values, not object instances.

// This throws TypeError - you cannot steal Symbol fairy's wand!
// const sym = new Symbol('test'); // TypeError: Symbol is not a constructor

// Correct way - make a wish to Symbol fairy
const sym = Symbol('test'); // ✓ Correct

Why this design choice?

  • Symbols are meant to be lightweight primitive values, not objects
  • Prevents confusion about whether symbols are objects or primitives
  • Ensures consistent behavior - symbols are always primitives
  • Avoids the overhead of object creation for what should be simple unique identifiers

Do We Really Need Symbols?

Use symbols when your requirement is one of these:

1. Enums

Create constants that are guaranteed to be unique:

const Colors = {
RED: Symbol('red'),
GREEN: Symbol('green'),
BLUE: Symbol('blue')
};

// These will never conflict with any other values
function processColor(color) {
switch (color) {
case Colors.RED:
return 'Stop!';
case Colors.GREEN:
return 'Go!';
default:
return 'Unknown';
}
}

2. Name Clashes

Prevent property conflicts in objects:

// Library A
const LibA_CONFIG = Symbol('config');

// Library B
const LibB_CONFIG = Symbol('config');

const app = {};
app[LibA_CONFIG] = { theme: 'dark' };
app[LibB_CONFIG] = { debug: true };

// No conflicts! Both configs coexist

3. Privacy

Create pseudo-private properties:

const _balance = Symbol('balance');
const _id = Symbol('id');

class BankAccount {
constructor(id, balance) {
this[_id] = id;
this[_balance] = balance;
}

getBalance() {
return this[_balance];
}
}

const account = new BankAccount('123', 1000);
console.log(account.getBalance()); // 1000
// account._balance is undefined - cannot access directly

4. Well-known Symbols

Customize object behavior:

const myIterable = {
data: [1, 2, 3],
[Symbol.iterator]: function* () {
for (let item of this.data) {
yield item;
}
}
};

for (let value of myIterable) {
console.log(value); // 1, 2, 3
}

Alternative approaches without symbols:

  • Use conventions like _private (not truly private)
  • Use WeakMaps for privacy (more complex)
  • Use string keys with unique prefixes (still can collide)

Why Were Symbols Needed?

Before symbols, JavaScript developers faced several challenges:

1. Property Name Collisions

When working with objects, especially in large applications or when integrating third-party libraries, property names could accidentally clash:

// Without symbols - potential conflicts
const user = { name: 'Alice' };
// Library A adds
user.id = 'lib-a-id';
// Library B overwrites it!
user.id = 'lib-b-id';

2. No True Privacy

JavaScript didn't have a built-in way to create truly private properties:

// Properties are always visible
const obj = { secret: 'hidden' };
console.log(obj.secret); // Anyone can access it

3. Need for Meta-Programming

JavaScript needed a way to add special behaviors to objects without interfering with normal properties.


Creating and Using Symbols

Basic Symbol Creation

// Create a symbol - Symbol fairy grants unique symbols
const mySymbol = Symbol();
const namedSymbol = Symbol('description');

// IMPORTANT: Cannot use 'new' keyword
// new Symbol(); // TypeError: Symbol is not a constructor

// The description is just for debugging
console.log(mySymbol.toString()); // "Symbol()"
console.log(namedSymbol.toString()); // "Symbol(description)"

Using Symbols as Object Properties

const id = Symbol('id');
const name = Symbol('name');

const user = {
[id]: 12345,
[name]: 'Alice',
email: 'alice@example.com'
};

console.log(user[id]); // 12345
console.log(user[name]); // Alice
console.log(user.email); // alice@example.com

Symbol Properties Are Hidden from Enumeration

const secret = Symbol('secret'); // first create Symbol, add as the property to the object
const obj = {
visible: 'everyone can see this',
[secret]: 'hidden treasure'
};

// Symbol is non-enumerable by nature
// Regular enumeration doesn't show symbols
console.log(Object.keys(obj)); // ['visible']
for (let key in obj) {
console.log(key); // Only logs 'visible'
}

// Object.getOwnPropertyNames() also doesn't include symbols
console.log(Object.getOwnPropertyNames(obj)); // ['visible']

// But you can still access it if you know the symbol
console.log(obj[secret]); // 'hidden treasure'

Global Symbol Registry (The Fairyland)

JavaScript provides a global symbol registry (the fairyland) for symbols that need to be shared across different parts of your application:

// Create or retrieve a symbol from global registry (fairyland)
const globalSym1 = Symbol.for('app.id');
const globalSym2 = Symbol.for('app.id');

console.log(globalSym1 === globalSym2); // true - same symbol from fairyland!

// Get the key for a global symbol
console.log(Symbol.keyFor(globalSym1)); // 'app.id'

// Regular symbols vs Global symbols
const regular1 = Symbol('test');
const regular2 = Symbol('test');
console.log(regular1 === regular2); // false - always unique

const global1 = Symbol.for('test');
const global2 = Symbol.for('test');
console.log(global1 === global2); // true - same from fairyland

When to use global symbols:

  • Cross-module communication
  • Plugin systems
  • Shared configuration keys

Well-Known Symbols

JavaScript provides several built-in symbols that control object behavior:

Symbol.iterator

Makes objects iterable:

const myIterable = {
data: ['a', 'b', 'c'],
[Symbol.iterator]: function* () {
for (let item of this.data) {
yield item;
}
}
};

for (let value of myIterable) {
console.log(value); // 'a', 'b', 'c'
}

Symbol.toStringTag

Customizes the Object.prototype.toString() result:

class MyClass {
get [Symbol.toStringTag]() {
return 'MyCustomClass';
}
}

const instance = new MyClass();
console.log(instance.toString()); // "[object MyCustomClass]"

Symbol.hasInstance

Customizes instanceof behavior:

class MyArray {
static [Symbol.hasInstance](instance) {
return Array.isArray(instance);
}
}

console.log([] instanceof MyArray); // true

Other Well-Known Symbols

  • Symbol.asyncIterator - Async iteration
  • Symbol.match - String matching behavior
  • Symbol.replace - String replacement behavior
  • Symbol.search - String search behavior
  • Symbol.split - String splitting behavior
  • Symbol.toPrimitive - Type conversion
  • Symbol.species - Constructor for derived objects

Practical Use Cases

1. Creating Private Properties

const _id = Symbol('id');
const _balance = Symbol('balance');

class BankAccount {
constructor(id, initialBalance) {
this[_id] = id;
this[_balance] = initialBalance;
}

getBalance() {
return this[_balance];
}

deposit(amount) {
this[_balance] += amount;
}
}

const account = new BankAccount('123', 1000);
console.log(account.getBalance()); // 1000
console.log(account[_balance]); // undefined (symbol not accessible)

2. Avoiding Library Conflicts

// Library A - gets unique symbol from Symbol fairy
const LibA_ID = Symbol('LibA.id');
function attachLibAData(obj, id) {
obj[LibA_ID] = id;
}

// Library B - gets different unique symbol
const LibB_ID = Symbol('LibB.id');
function attachLibBData(obj, id) {
obj[LibB_ID] = id;
}

// Usage - no conflicts because Symbol fairy ensures uniqueness!
const user = {};
attachLibAData(user, 'a-123');
attachLibBData(user, 'b-456');
// Both IDs coexist peacefully

3. Meta-Programming

const validators = Symbol('validators');

class User {
constructor(name, email) {
this.name = name;
this.email = email;
this[validators] = [];
}

addValidator(fn) {
this[validators].push(fn);
}

validate() {
return this[validators].every(validator => validator(this));
}
}

4. Enums with Symbols

const Colors = {
RED: Symbol('red'),
GREEN: Symbol('green'),
BLUE: Symbol('blue')
};

function processColor(color) {
switch (color) {
case Colors.RED:
return 'Stop!';
case Colors.GREEN:
return 'Go!';
case Colors.BLUE:
return 'Cool!';
default:
return 'Unknown color';
}
}

Symbol vs String Keys

AspectSymbolsStrings
UniquenessAlways unique (Symbol fairy magic)Can collide
EnumerationHidden from for...in, Object.keys()Visible in enumeration
JSON.stringifyIgnoredIncluded
Property accessobj[symbol] onlyobj.key or obj['key']
DebuggingHas optional descriptionSelf-descriptive
CreationSymbol() or Symbol.for()'string' or new String()
Type conversionCannot convert to string/numberCan convert

Getting Symbol Properties

While symbols are hidden from normal enumeration, you can still access them if needed:

const sym1 = Symbol('prop1');
const sym2 = Symbol('prop2');

const obj = {
normal: 'visible',
[sym1]: 'hidden1',
[sym2]: 'hidden2'
};

// Get symbol properties specifically
console.log(Object.getOwnPropertySymbols(obj)); // [sym1, sym2]

// Get all property descriptors (including symbols)
console.log(Reflect.ownKeys(obj)); // ['normal', sym1, sym2]

// Regular methods don't show symbols
console.log(Object.keys(obj)); // ['normal']
console.log(Object.getOwnPropertyNames(obj)); // ['normal']

Best Practices

1. Use Descriptive Names

// Good - describe your wish clearly to Symbol fairy
const userId = Symbol('user.id');
const isAdmin = Symbol('user.isAdmin');

// Not so good
const a = Symbol();
const b = Symbol('x');

2. Keep Symbol References

// Store symbols for later use - Symbol fairy doesn't repeat wishes
const SYMBOLS = {
ID: Symbol('id'),
ROLE: Symbol('role'),
PERMISSIONS: Symbol('permissions')
};

// Use throughout your application
user[SYMBOLS.ID] = 12345;

3. Use Global Registry (Fairyland) Sparingly

// Only for truly global, shared symbols - fairyland access
const GLOBAL_EVENT_BUS = Symbol.for('app.eventBus');

// Prefer local symbols for most use cases
const localSecret = Symbol('secret');

4. Document Symbol Usage

/**
* @private
* @symbol _internalState - Stores internal component state
*/
const _internalState = Symbol('internalState');

Common Pitfalls

1. Symbols Are Not Strings

const sym = Symbol('test');
console.log(typeof sym); // 'symbol', not 'string'
console.log(sym == 'test'); // false
console.log(sym + ''); // TypeError: Cannot convert a Symbol value to a string

2. Symbol Descriptions Are Just Hints

const sym1 = Symbol('same');
const sym2 = Symbol('same');
console.log(sym1 === sym2); // false - Symbol fairy gives unique symbols despite same description

3. JSON Serialization Ignores Symbols

const obj = {
normal: 'visible',
[Symbol('hidden')]: 'invisible'
};

console.log(JSON.stringify(obj)); // {"normal":"visible"}

Why does JSON.stringify ignore symbols?

  • JSON format doesn't support symbols as they are a JavaScript-specific primitive type
  • Symbols are meant for internal object mechanics, not for data exchange
  • Including symbols in JSON would break interoperability with other languages

Converting Symbols to Strings

If you need to include symbol-keyed properties in JSON, you have a few options:

Option 1: Manual conversion with custom logic

const sym = Symbol('id');
const obj = {
name: 'Alice',
[sym]: 12345
};

// Convert symbol properties manually
function serializeWithSymbols(obj) {
const result = { ...obj }; // copy regular properties

// Get symbol properties and convert them
Object.getOwnPropertySymbols(obj).forEach(symbol => {
// Use symbol description as string key
const key = symbol.description || symbol.toString();
result[key] = obj[symbol];
});

return JSON.stringify(result);
}

console.log(serializeWithSymbols(obj)); // {"name":"Alice","id":12345}

Option 2: Custom replacer function

const sym = Symbol('secret');
const obj = {
visible: 'data',
[sym]: 'hidden data'
};

function symbolReplacer(key, value) {
if (typeof key === 'symbol') {
return `Symbol(${key.description})`;
}
return value;
}

// This still won't include symbol properties, but shows the concept
console.log(JSON.stringify(obj, symbolReplacer));

Option 3: Use a transformation function

const sym1 = Symbol('id');
const sym2 = Symbol('type');
const obj = {
name: 'Test',
[sym1]: 123,
[sym2]: 'user'
};

function transformSymbolsToStrings(obj) {
const transformed = {};

// Copy regular properties
Object.keys(obj).forEach(key => {
transformed[key] = obj[key];
});

// Convert symbol properties to string keys
Object.getOwnPropertySymbols(obj).forEach(symbol => {
const stringKey = `__symbol_${symbol.description || 'unnamed'}`;
transformed[stringKey] = obj[symbol];
});

return transformed;
}

const serializable = transformSymbolsToStrings(obj);
console.log(JSON.stringify(serializable));
// {"name":"Test","__symbol_id":123,"__symbol_type":"user"}

4. Cannot Use 'new' Keyword

// This throws TypeError - you cannot steal Symbol fairy's wand!
// const sym = new Symbol('test'); // TypeError: Symbol is not a constructor

// Correct way - make a wish to Symbol fairy
const sym = Symbol('test'); // ✓ Correct

5. Symbols Are Non-Enumerable

const sym = Symbol('hidden');
const obj = { visible: 'shown', [sym]: 'hidden' };

// These won't show the symbol property
for (let key in obj) { /* only 'visible' */ }
Object.keys(obj); // ['visible']
Object.getOwnPropertyNames(obj); // ['visible']

// Use these to find symbols
Object.getOwnPropertySymbols(obj); // [sym]
Reflect.ownKeys(obj); // ['visible', sym]

Advanced Symbol Patterns

Creating a Symbol-Based Event System

const EventSymbols = {
LISTENERS: Symbol('listeners'),
EMIT: Symbol('emit'),
ON: Symbol('on'),
OFF: Symbol('off')
};

function makeEventEmitter(obj) {
obj[EventSymbols.LISTENERS] = new Map();

obj[EventSymbols.ON] = function(event, listener) {
if (!this[EventSymbols.LISTENERS].has(event)) {
this[EventSymbols.LISTENERS].set(event, []);
}
this[EventSymbols.LISTENERS].get(event).push(listener);
};

obj[EventSymbols.EMIT] = function(event, ...args) {
const listeners = this[EventSymbols.LISTENERS].get(event) || [];
listeners.forEach(listener => listener(...args));
};

return obj;
}

// Usage
const emitter = makeEventEmitter({});
emitter[EventSymbols.ON]('test', data => console.log('Received:', data));
emitter[EventSymbols.EMIT]('test', 'Hello World!');

Summary

Symbols are JavaScript's answer to creating truly unique identifiers that:

  • Prevent property collisions in objects (Symbol fairy ensures uniqueness)
  • Enable meta-programming through well-known symbols
  • Provide pseudo-privacy for object properties
  • Support better library design by avoiding naming conflicts
  • Are non-enumerable by nature - hidden from normal iteration

Do we really need symbols? Use symbols when your requirement is one of these:

  • Enums - Unique constants
  • Name Clashes - Avoiding property conflicts
  • Privacy - Pseudo-private properties
  • Well-known Symbols - Customizing object behavior

Remember the Symbol Fairy Rules:

  1. Symbol fairy distributes unique symbols - Symbol() always returns unique values
  2. Use her wand correctly - Symbol(), not new Symbol()
  3. Global registry (fairyland) - Symbol.for() for shared symbols
  4. Descriptions are just hints - Symbol('same') !== Symbol('same')
  5. Every symbol is unique - except those stored in the fairyland
  6. Symbols are non-enumerable - hidden from for...in, Object.keys()
  7. Symbols cannot be converted - to strings or numbers
  8. JSON ignores symbols - use manual conversion if needed for serialization

Remember: Every symbol is unique, descriptions are optional hints, symbols are non-enumerable by nature, and symbols are your friends for creating better, more maintainable JavaScript applications.