How and why to use Symbols in Javascript

Symbols are a primitive data type in Javascript introduced in the ES2015 specification. It is the seventh data type in Javascript, joining String, Number, Boolean, Object, undefined and null. The use case for Symbol is relatively limited, but it is worth knowing about since it is one of the few primitive data types.

How to Use Symbol

A symbol is created by calling the Symbol function. It accepts one optional parameter, which is a description. It’s important to realise that the only purpose of the description is to aid in debugging. That is, when you console.log your symbol, it will also show the description. The description does not affect the operation of the symbol itself in any way.

Every symbol you create is a unique reference. Two symbols will never be the same.

let symbolA = Symbol();
let symbolB = Symbol('newInWeb');
let symbolC = Symbol('newInWeb');

console.log(symbolA.toString()); // 'Symbol()'
console.log(symbolB.toString()); // 'Symbol(newInWeb)
console.log(symbolC.toString()); // 'Symbol(newInWeb)

// typeof will return the new data type, 'symbol'
console.log(typeof symbolA); // 'symbol'

// despite having the same description, B and C are not the same
console.log(symbolB === symbolC); // false

Why to Use Symbols

A lot of Javascript developers know that symbols exist, but are unsure why to use them. The main use case is private properties, and the second is to help avoid accidental overrides in classes and objects.

Private properties

Imagine a user object which contains a name and id. How do you prevent id from being acccessed? In the early days of Javascript, you would put an underscore in front of id to hint to the developer that this property is private. But, it does nothing to stop them accessing it.

In 2009, ES5 introduced the concept of non-enumerable properties. This means that a property will not be returned when enumerating the object - that is, when using a function like Object.keys or a loop like for(let i in obj). A developer won’t programatically stumble upon it, but it doesn’t change the fact that the value is still easily accessed by knowing its key.

let user = {
  name: 'Miles'
};
Object.defineProperty(user, '_id', { value: 123456, enumerable: false });

console.log(Object.keys(user)); // [ "name" ]
console.log(user._id); // 123456

This is where symbols come in. Instead of using a string as the key, we can use a symbol. Symbols are completely unique, and you cannot access the property unless you have access to the symbol which is its key.

let idSymbol = Symbol('_id');
const user = {
  [idSymbol]: 123456,
  name: 'Miles'
};

console.log(Object.keys(user)); // [ "name" ]
console.log(user._id) // undefined
console.log(user[idSymbol]); // 123456

Of course, this isn’t meant for security, just to ensure that the property is very obviously private and should be left alone. The symbol-based keys of any object can be easily found by:

console.log(Object.getOwnPropertySymbols(user)); // [Symbol(_id)]

Avoiding accidental class overrides

If you are writing a library, you will want to do all you can to ensure that users can’t accidentally break it. Take the following example, where the User class is provided by your library, and the developer has created a Member class which extends it. They have created a setup function, unknowingly named the same as yours. The result is that your setup never gets called, and the library breaks, getting you a fun bug report to deal with.

class User {
  constructor() {
    this.setup();
  }

  setup() {
    console.log('Setting up user');
  }
}

class Member extends User {
  constructor() {
    super();
    this.setup();
  }

  setup() {
    console.log('Setting up member');
  }
}

let user = new User();
// Expected: 'Setting up user'
// Result:   'Setting up user'

let member = new Member();
// Expected: 'Setting up user', 'Setting up member'
// Result:   'Setting up member', 'Setting up member'

To avoid this problem, you can use a symbol as the key for your setup function. This way, the user will never unknowingly override your function.

const setupSymbol = Symbol('setup');
class User {
  constructor() {
    this[setupSymbol]();
  }

  [setupSymbol]() {
    console.log('Setting up user');
  }
}

class Member extends User {
  constructor() {
    super();
    this.setup();
  }

  setup() {
    console.log('Setting up member');
  }
}

let user = new User();
// Expected: 'Setting up user'
// Result:   'Setting up user'

let member = new Member();
// Expected: 'Setting up user', 'Setting up member'
// Result:   'Setting up user', 'Setting up member'

Do I need to use symbols?

Symbols can be a good way of avoiding bugs caused by accidental overrides or modification of private variables. If you are working on a large, shared codebase, or on a popular library, it could be worth using them. Just ensure that your colleagues are familiar with how to use them, so that everyone is clear on how they work and what they are doing for you.

As always, if you have any questions or suggestions, please let me know!

Other recent posts: