Skip to main content

JavaScript Symbols

Introduction to Symbols

Symbols are primitive values that represent unique identifiers. They were introduced in ES6 (ES2015) and provide a way to create non-string property keys for objects.

Creating Symbols

Basic Symbol Creation

// Creating a simple Symbol
const sym1 = Symbol();
const sym2 = Symbol();
console.log(sym1 === sym2); // false

// Symbol with description
const sym3 = Symbol('mySymbol');
console.log(sym3.description); // 'mySymbol'

// Symbols are always unique
const sym4 = Symbol('mySymbol');
console.log(sym3 === sym4); // false, even with same description

Symbol.for() and Symbol.keyFor()

// Creating shared Symbols using Symbol.for()
const globalSym1 = Symbol.for('globalSymbol');
const globalSym2 = Symbol.for('globalSymbol');
console.log(globalSym1 === globalSym2); // true

// Getting symbol key using Symbol.keyFor()
console.log(Symbol.keyFor(globalSym1)); // 'globalSymbol'

// Regular symbols are not registered
const localSym = Symbol('localSymbol');
console.log(Symbol.keyFor(localSym)); // undefined

Using Symbols as Object Properties

Basic Property Usage

const MY_KEY = Symbol('myKey');
const obj = {
[MY_KEY]: 'Symbol value',
regularKey: 'Regular value'
};

console.log(obj[MY_KEY]); // 'Symbol value'
console.log(Object.keys(obj)); // ['regularKey']
console.log(Object.getOwnPropertySymbols(obj)); // [Symbol(myKey)]

Symbol Property Enumeration

const obj = {
[Symbol('a')]: 'a',
[Symbol('b')]: 'b',
c: 'c'
};

// Different ways to access properties
console.log(Object.keys(obj)); // ['c']
console.log(Object.getOwnPropertyNames(obj)); // ['c']
console.log(Object.getOwnPropertySymbols(obj)); // [Symbol(a), Symbol(b)]

// Getting all properties including symbols
const allProps = [
...Object.getOwnPropertyNames(obj),
...Object.getOwnPropertySymbols(obj)
];

Well-Known Symbols

Symbol.iterator

class CustomCollection {
constructor() {
this.items = [];
}

add(item) {
this.items.push(item);
}

// Making object iterable
[Symbol.iterator]() {
let index = 0;
return {
next: () => {
if (index < this.items.length) {
return { value: this.items[index++], done: false };
}
return { done: true };
}
};
}
}

const collection = new CustomCollection();
collection.add('a');
collection.add('b');

for (const item of collection) {
console.log(item); // 'a', 'b'
}

Symbol.toStringTag

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

const obj = new CustomClass();
console.log(Object.prototype.toString.call(obj)); // '[object CustomClass]'

Symbol.toPrimitive

const obj = {
[Symbol.toPrimitive](hint) {
switch (hint) {
case 'number':
return 42;
case 'string':
return 'Custom string';
default:
return 'Default value';
}
}
};

console.log(+obj); // 42
console.log(`${obj}`); // 'Custom string'
console.log(obj + ''); // 'Default value'

Advanced Use Cases

Private Properties (Pre-Class Fields)

const privateProps = new WeakMap();

class PrivateClass {
constructor() {
privateProps.set(this, {
secret: 'private data'
});
}

getSecret() {
return privateProps.get(this).secret;
}
}

const instance = new PrivateClass();
console.log(instance.getSecret()); // 'private data'
console.log(privateProps.get(instance).secret); // 'private data'

Custom Object Types

const TYPE = Symbol('type');

class ValidationError extends Error {
constructor(message) {
super(message);
this[TYPE] = 'ValidationError';
}

get type() {
return this[TYPE];
}
}

const error = new ValidationError('Invalid input');
console.log(error.type); // 'ValidationError'

Registry Pattern

const Registry = {
_registry: new Map(),

register(key, value) {
const sym = Symbol.for(key);
this._registry.set(sym, value);
return sym;
},

get(key) {
return this._registry.get(Symbol.for(key));
}
};

// Usage
Registry.register('config', { env: 'production' });
console.log(Registry.get('config')); // { env: 'production' }

Symbol Metadata and Reflection

Property Descriptors with Symbols

const sym = Symbol('test');
const obj = {};

Object.defineProperty(obj, sym, {
value: 'Symbol value',
writable: true,
enumerable: false,
configurable: true
});

console.log(Object.getOwnPropertyDescriptor(obj, sym));
// {
// value: 'Symbol value',
// writable: true,
// enumerable: false,
// configurable: true
// }

Symbol Property Reflection

const symbols = Object.getOwnPropertySymbols(obj);
const symbolProps = symbols.reduce((acc, sym) => {
acc[sym.description] = obj[sym];
return acc;
}, {});

Best Practices

  1. Use Descriptive Names
// Good
const RENDER_MODE = Symbol('renderMode');

// Avoid
const s = Symbol();
  1. Symbol Registry Management
// Centralized symbol management
const AppSymbols = {
events: {
INIT: Symbol('app.events.init'),
READY: Symbol('app.events.ready')
},
config: {
ENV: Symbol('app.config.env'),
MODE: Symbol('app.config.mode')
}
};
  1. Documentation
/**
* @typedef {Symbol} RenderMode
* Represents the rendering mode of the component
* @property {Symbol} SYNC - Synchronous rendering
* @property {Symbol} ASYNC - Asynchronous rendering
*/
const RenderMode = {
SYNC: Symbol('RenderMode.SYNC'),
ASYNC: Symbol('RenderMode.ASYNC')
};

Symbols provide a powerful way to create unique identifiers and implement special behavior in JavaScript objects. They're particularly useful for:

  • Creating non-string property keys
  • Implementing well-known behaviors
  • Building extensible systems
  • Managing private properties
  • Creating unique identifiers for registration systems

Remember that Symbols are not private in themselves - they're simply unique identifiers that can be accessed using the appropriate methods.