DEV Community

loading...
Cover image for Explain Me Like I am Five: What are ES6 Symbols?

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

atapas profile image Tapas Adhikary Originally published at blog.greenroots.info Updated on ・8 min read

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');
Enter fullscreen mode Exit fullscreen mode

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 later 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;
Enter fullscreen mode Exit fullscreen mode

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';
Enter fullscreen mode Exit fullscreen mode

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');
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

Also,

typeof Symbol() // is "symbol"
Enter fullscreen mode Exit fullscreen mode

Where can I use Symbols?

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

⭐ A unique value where we normally use a string identifier.

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';
Enter fullscreen mode Exit fullscreen mode

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!!`;
      }
  }
Enter fullscreen mode Exit fullscreen mode

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));
Enter fullscreen mode Exit fullscreen mode

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 are passed(example, PLANET_EARTH) while invoking the function.

As we are dealing with string type here, they are not unique. This may lead to bugs and confusions. 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');
Enter fullscreen mode Exit fullscreen mode

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

console.log(getPlanetInformation(PLANET_EARTH));
Enter fullscreen mode Exit fullscreen mode

The output will be,

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

But the following invocation will result into an error,

 console.log(getPlanetInformation(Symbol('Earth')));
Enter fullscreen mode Exit fullscreen mode

Output,

Error: Unknown planet. Mostly Alien lives there!!
Enter fullscreen mode Exit fullscreen mode

⭐ 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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

We can also do it with method definition,

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

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

Symbols are not visible straightway as the string-based keys are.

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'
};
Enter fullscreen mode Exit fullscreen mode

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));
Enter fullscreen mode Exit fullscreen mode

The output,

["address"]
[Symbol]
["address", Symbol]
["address"]
Enter fullscreen mode Exit fullscreen mode

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 are not required to fetch out when we enumerate the object.

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

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);
Enter fullscreen mode Exit fullscreen mode

Output,

Symbol {}
Enter fullscreen mode Exit fullscreen mode

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)')
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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)'
Enter fullscreen mode Exit fullscreen mode

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 shows the conversion list (taken from the exploringJS book),

image.png

Reusable Symbols

Symbols are completely unique, except in a special situation. Symbols can be created in a global symbol registry and fetch 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 into 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.
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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 to 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 to 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 are the list of well-known symbols:

  • Symbol.hasInstance: It drives the behavior of instanceof.
  • Symbol.iterator: It allows us to override the of operator of the for-of loop.
  • Symbol.unscopables: It defines a set of “unscopable” values in an Object which should not be set when used inside the with statement.
  • Symbol.match: Use to provide your own matching implementation, rather than using Regular Expressions.
  • Symbol.toPrimitive: It is used when the JavaScript engine needs to convert your Object into a primitive value.
  • Symbol.toStringTag: It helps in overriding toString().
  • Symbol.species: It points to the constructor value of a class, which allows classes to create new versions of themselves.
  • Symbol.split: It is for String#split.
  • Symbol.search: It helps overriding String#search by allowing for custom classes instead of Regular Expressions:
  • Symbol.replace: It allows custom classes for String#replace, where you’d normally use Regular Expressions.
  • Symbol.isConcatSpreadable: It is to determine if Array#concat has any of its arguments that are spreadable.

Summary

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

To summarize it,

  • Symbols are added as a feature to ES6.
  • Symbols are mostly unique, except when created in the global registry.
  • The uniqueness of symbols make it 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. I am passionate about UI/UX and love sharing my knowledge through articles. Please visit my blog to know more.

You may also like,

Follow me on twitter @tapasadhikary for any technical discussions.

Discussion

pic
Editor guide
Collapse
devworkssimone profile image
DevWorksSimone

Cannot read all of it at 4am in the morning will do it in Later, seems very interesting. I was struggling to know why they were introduced and the usecase for it. Also saw them in the react library and thought this were magic crafting 😂

Collapse
atapas profile image
Tapas Adhikary Author

Haha... Thanks...

Hope you get a chance to read it later..

Collapse
crisarji profile image
crisarji

Really nice explanation!, thanks a lot!..

Collapse
atapas profile image
Tapas Adhikary Author

Thank you... Made my day 🙏

Collapse
geomc profile image
Sascha

A well written article, thanks :)

Collapse
atapas profile image
Collapse
remidej profile image
Rémi de Juvigny

Thanks for the explanation! I love that you mention different use cases. It makes it easier to understand

Collapse
atapas profile image
Tapas Adhikary Author

Thank you very much!