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');
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,
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 @,
-
atapas / knowing-es6-symbols
Code examples to understand the ES6 Symbol better!
knowing-es6-symbols
- ES6 Symbol - DemoLab
If it was useful to you, please Like/Share so that, it reaches others as well.
You may also like,
Follow me on twitter @tapasadhikary
Top comments (8)
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 😂
Haha... Thanks...
Hope you get a chance to read it later..
Really nice explanation!, thanks a lot!..
Thank you... Made my day 🙏
A well written article, thanks :)
Thank you 🙏
Thanks for the explanation! I love that you mention different use cases. It makes it easier to understand
Thank you very much!