Explain Me Like I am Five: What are ES6 Symbols?

Explain Me Like I am Five: What are ES6 Symbols?

Introduction

Symbol is a primitive type(not an object) included in the ECMAScript 2015(aka, ES6). We are already familiar with the existing primitive types like, Number, String and, Boolean. Like these primitive types, Symbols are also created via a factory function,

const sym = Symbol('Symbol Name');

Note, the parameter 'Symbol Name' can be any string and it is optional. It has no impact on the symbol being created other than helping the developers in debugging. We will see that in the latter part of this article.

There is a specific difference in the creation pattern of a Symbol and other primitive types. All other primitive types have literals. For example, the Boolean type has two literal values: true and false. So, we can do like,

let shouldJump = false;
let shouldEat = true;

A string literal is zero or more characters enclosed in double (") or single (') quotation marks. We can do like,

let name = 'tapas';
let address = 'somewhere';

But, you can not do the same with Symbol. You need to create symbols by calling the function Symbol(). Please note, it is not a constructor. Hence, you can not use the new keyword to create a symbol.

// This will not work!
const sym = new Symbol('Symbol Name');

image.png

But, what is so special about Symbols?

Symbol allows us to create unique identifiers. Every time we invoke Symbol(), a new unique symbol is created. Two symbols are not equal(they are unique) even when they have the same name,

let symA = Symbol();
let symB =Symbol();
(symA === symB) // false

let symAWithName = Symbol('Name');
let symBWithName = Symbol('Name');
(symAWithName === symBWithName ) // false

Also,

typeof Symbol() // is "symbol"

Where can I use Symbols?

As symbols are completely unique, there is some interesting usage of them.

⭐ Symbols as unique identifiers

Consider this example where we are trying to get information about a planet by passing the planet as an identifier.

First, we create the constants with the planet identifiers. We are using the string based identifier to find the planet information.

const PLANET_MERCURY = 'Mercury';
const PLANET_MARS = 'Mars';
const PLANET_VENUS = 'Venus';
const PLANET_EARTH  = 'Earth';
const PLANET_NEPTUNE   = 'Neptune';
const PLANET_URANUS = 'Uranus';
const PLANET_SATURN = 'Saturn';
const PLANET_JUPITER = 'Jupiter';

Next, a function to get the information about the planet,

function getPlanetInformation(planet) {
      switch (planet) {
          case PLANET_MERCURY:
              return `Mercury is 38% the size of Earth. 
                            It is 2,440 km / 1,516 miles`;
          case PLANET_MARS:
              return `Mars is 53% the size of Earth. 
                            It is 3,390 km / 2,460 miles`;
          case PLANET_VENUS:
              return `Venus is 95% the size of Earth. 
                            It is 6,052 km / 3,761 miles`;
          case PLANET_EARTH:
              return `We live here, this is Earth. 
                            It is 6,371 km / 3,959 miles`;
          case PLANET_NEPTUNE:
              return `Neptune is 388% the size of Earth. 
                            It is 24,622 km / 15,299 miles`;
          case PLANET_URANUS:
              return `Uranus is 400% the size of Earth. 
                            It is 25,362 km / 15,759 miles`;
          case PLANET_SATURN:
              return `Saturn is 945% the size of Earth. 
                            It is 58,232 km / 36,184 miles`;
          case PLANET_JUPITER:
              return `Jupiter is 1,120% the size of Earth. 
                            It is 69,911 km / 43,441 miles`;
          default:
              return `Error: Unknown planet. Mostly Alien lives there!!`;
      }
  }

As we have the function ready, there are multiple ways to get the planet information. We can do,

console.log(getPlanetInformation(PLANET_EARTH));

// or,
console.log(getPlanetInformation('Earth'));

// or,
let input = 'Earth';
console.log(getPlanetInformation(input));

All the above will output, We live here, this is Earth. It is 6,371 km / 3,959 miles.

This is not ideal. You would expect it to throw an error or not to provide the information when anything other than the expected identifiers is passed(example, PLANET_EARTH) while invoking the function.

As we are dealing with the string type here, they are not unique. This may lead to bugs and confusion. So how do we solve it? Use Symbol instead.

The only change required in the code above is, declare the identifiers as Symbol than string.

const PLANET_MERCURY = Symbol('Mercury');
const PLANET_MARS = Symbol('Mars');
const PLANET_VENUS = Symbol('Venus');
const PLANET_EARTH  = Symbol('Earth');
const PLANET_NEPTUNE   = Symbol('Neptune');
const PLANET_URANUS = Symbol('Uranus');
const PLANET_SATURN = Symbol('Saturn');
const PLANET_JUPITER = Symbol('Jupiter');

That's all. Rest of the code can stay as is. Now if we do,

console.log(getPlanetInformation(PLANET_EARTH));

The output will be,

We live here, this is Earth. It is 6,371 km / 3,959 miles

But the following invocation will result into an error,

 console.log(getPlanetInformation(Symbol('Earth')));

Output,

Error: Unknown planet. Mostly Alien lives there!!

⭐ Symbols as Object property keys

Symbols can be assigned as a key to an object. This will make sure, the object keys are unique and there are no chances of the object key clashing. Usually, object keys are string types. In contrast to string, symbols are unique and prevent name clashes.

const MY_KEY = Symbol();
const obj = {};

obj[MY_KEY] = 'some_key';
console.log(obj[MY_KEY]); // some_key

You can specify the key of a property via an expression, by putting it in square brackets.

let MY_KEY_SYM = Symbol();
  let obj = {
    [MY_KEY_SYM] : 'Tapas'
}
console.log(obj[MY_KEY_SYM]); // Tapas

We can also do it with method definition,

let obj2 = {
    [MY_KEY_SYM](){
      return 'GreenRoots'
    }
}
console.log(obj2[MY_KEY_SYM]()); // GreenRoots

As symbols can be used as a key of an object, we need to be aware of how to enumerate them.

Here is an object with two properties. One with Symbol as key and another one is regular string-based key.

let obj = {
    [Symbol('name')]: 'Tapas',
    'address': 'India'
};

What do you think, the output of the following lines?

console.log(Object.getOwnPropertyNames(obj));
console.log(Object.getOwnPropertySymbols(obj));
console.log(Reflect.ownKeys(obj));
console.log(Object.keys(obj));

The output,

["address"]
[Symbol]
["address", Symbol]
["address"]

There are only a couple of ways we can enumerate on symbols,

  • Using the getOwnPropertySymbols(obj) method
  • Using the Reflect.ownKeys(obj) API.

⭐ Symbols as Object meta data

We can use symbols as object keys and it is not enumerable using regular ways of, Objet.keys(obj), Object.getOwnPropertyNames(obj). So it means, we can store some secondary information(like, metadata) that is not required to fetch out when we enumerate the object.

let obj = {
    [Symbol('created-at')]: '1599568901',
    'address': 'India',
    'name': 'Tapas'
};

Here the property created-at is the metadata information of the object. Hope it makes sense.

Symbols have debuggability

Try this,

let aSymbol = Symbol('A Symbol');
console.log(aSymbol);

Output,

Symbol {}

If you just have one symbol, in the entire application, not a problem. I am sure, that will be a rare case. When you have multiple symbols, getting an output like the above could be confusing.

The parameter(symbol name) we pass while creating a Symbol may be useful for debugging and identifying a symbol correctly.

console.log(Symbol('A Symbol').toString() === 'Symbol(A Symbol)')

The above code returns true.

Converting Symbols to other primitive types

You can’t coerce symbols to strings. Coerce means implicitly converting from one type to another.

const sym = Symbol('My Symbol');

const str1 = '' + sym; // TypeError
const str2 = `${sym}`; // TypeError

However, you will be able to do an explicit conversion.

const sym = Symbol('My Symbol');

const str1 = String(sym); // 'Symbol(My Symbol)'
const str2 = sym.toString(); // 'Symbol(My Symbol)'

This is probably the most useful conversion one should be aware of. But there are other types of implicit and explicit conversions you may want to know. Here is a table that shows the conversion list,

image.png Credit: Screenshot from exploringJS book

Reusable Symbols

Symbols are completely unique, except in a special situation. Symbols can be created in a global symbol registry and fetched from it. This feature enables you to create and share a symbol within an application and beyond.

This registry is cross-realm. It means a symbol created in the global registry from the current application frame will be accessible from an iframe or service worker.

Use Symbol.for() to create a symbol in the global registry. Note, if a symbol is created multiple times using the same name in the global registry, it returns the already created one.

console.log(Symbol('aSymbol') === Symbol('aSymbol')); // false, as they are local symbols.
console.log(Symbol.for('aSymbol') === Symbol.for('aSymbol')); // true, as created in the global registry.

How do we know, if a symbol has been created locally or globally? We have another useful method called, Symbol.keyFor. Check this out,

let globalASymbol = Symbol.for('aSymbol');
let localASymbol = Symbol('aSymbol');

console.log(Symbol.keyFor(globalASymbol)); // aSymbol
console.log(Symbol.keyFor(localASymbol)); // undefined

Is it worth knowing about Symbols?

Yes, it is. Symbols are a great tool to create uniqueness for keys, properties, variables. If you look back at your application, you will surely find places to incorporate symbols.

Apart from whatever we have learned so far, there are some "well-known" symbols. These are a bunch of static properties of the Symbol class. These are implemented within other JavaScript objects, such as Arrays, Strings, and also within the internals of the JavaScript engine.

The good news is, you can override them and make it as per your own implementations. Please note, the detailed explanations of these well-known symbols are outside of the scope of this article. But, we need to know them at a high level, at least. A future article will cover them in depth.

Here is the list of well-known symbols:

  • Symbol.hasInstance
  • Symbol.iterator
  • Symbol.unscopables
  • Symbol.match
  • Symbol.toPrimitive
  • Symbol.toStringTag
  • Symbol.species
  • Symbol.split
  • Symbol.search
  • Symbol.replace.
  • Symbol.isConcatSpreadable

Please check them in detail from the MDN site.

Summary

Symbol sounds complex but it is not. I wanted to explain the concept and usage of symbols in a simple way as possible. Please let me know if I was successful. A future article will explain the well-known symbols in detail.

To summarize,

  • Symbols are added as a feature to ES6.
  • Symbols are mostly unique, except when created in the global registry.
  • The uniqueness of symbols makes them useful as object properties, feature detection(the planet example), and defining the metadata of an object.
  • Symbols can be created using the function, Symbol() which optionally takes a name as an argument.
  • Symbols are not coercible into primitives(except boolean). It is object-coercible, it coerces it to an object.
  • With Well-Known symbols we can override the native implementation of JavaScript. It helps in achieving metaprogramming with JavaScript.

All the code used in this article can be found @,


If it was useful to you, please Like/Share so that, it reaches others as well. To get email notification on my latest posts, please subscribe to my blog by hitting the Subscribe button at the top of the page.

You may also like,

Follow me on twitter @tapasadhikary for any technical discussions.

Did you find this article valuable?

Support GreenRoots Blog - Tapas Adhikary by becoming a sponsor. Any amount is appreciated!