A practical guide to ES6 Symbol

A Symbol
Originally meant to introduce private properties to ES6, Symbols offer an approach to metaprogramming in Javascript that provides extension hooks into language operators and methods without risking user name collisions. A symbol
is a primitive data type that is immutable and globally-unique.
There are three kinds of Symbols:
- User-defined symbols created with the
Symbol
function - Globally-registered symbols created with the
Symbol.for
function - Well-known symbols defined as static properties on the Symbol object
While Symbols provide hooks into language methods, this same feature can be exploited by the developers of libraries and frameworks as well.
Here is a quick summary of how Symbols are used in code.
A user-defined symbol is created with the global Symbol
function
They are new to ES6, but not that new
No two symbols are alike
Except when they are equal (i.e. symbols in the global registry)
They really are their own type
They can be set as properties on objects
They can usually be overwritten, even in native object prototypes
They can keep data from prying eyes
Except they are still discoverable through reflection
These are user-defined symbols, originally meant to introduce near-private properties to Javascript. Then there are built-in symbols (aka well-known symbols) designed to provide hooks into the implementation of native functions without risking clashes with user-defined names.
Well-known Symbols
Some Symbols are more well-known than others. There are symbols like Symbol.iterator
that allow developers control over how an object can be iterated and spread. Then there are symbols like Symbol.unscopables
, which was born out of a need to maintain backwards compatibility with the seldom used with
keyword.
As an example, consider how to write a Password
class that:
- Accepts an unmasked password string
- Irreversibly masks the password
- Can match (true or false) against a test password
In writing this class, we will use built-in symbols.
To simplicity sake, we will implement the actually “hashing” using an insecure method based on Java’s Object.hashCode
function. For secure hashing, consider using a dedicated cryptography library like CryptoJs.
hashCode
irreversibly turns a String into a Number with some probability of collision (i.e. two Strings hash into the same Number).
With our hashCode
function, the first task is to mask the real password.
As we’ve seen before, a Symbol
can be used as a property on an Object and is generally more private than say a String property prefixed with an underscore. However, it is not completely private. We can either use a direct reference to PWD
or use reflective methods like Object.getOwnPropertySymbols
to obtain a reference to our unique symbol.
For more private properties, we can use IIFE or WeakMap and soon ES7 will support a language-level private field modifier #
.
Symbol.match
The next task is to make Passwords
comparable. Symbol.match
provides a hook into String.match
, which is one way for comparing strings. It is generally used to compare against a regular expression and the native implementation even implicitly coerces values into RegExp
. However, our Password
class will explicitly define its own behavior.
Now we can compare using the Password as we would a RegExp
!
Symbol.toPrimitive
While the example above could be achieved without Symbols (either as its own method or by overwriting String.prototype
), there are well-known Symbols that alter language and operator behavior like type conversion.
This is where Symbol.toPrimitive
comes in. It is used to convert Objects to primitives and is called by operators like the unary plus operator. We will use this Symbol to convert our Password into a masked String.
Now when we coerce our Password into a String, we get asterisks.
The hint parameter hint
can be “number”, “string”, or “default”. A more complete implementation could take these values into account and return NaN
or other values, as applicable. For now, we will only support String.
When we try to turn our password back into a string we end up with a masked password with asterisks replacing each character.
It is worth noting that Symbol.toPrimitive
is used for more than explicit type conversion. It is also used for implicit coercions done by the additional operator +
as well as the equality operator ==
.
Symbol.toStringTag
You might notice that if instead we call Object.toString
on our password, we get the generic tag [object Object]
. Symbols allow us to be more descriptive. Using the Symbol.toStringTag
symbol, we can provide the Javascript runtime with a better tag for our object.
Interestingly, Symbol.toStringTag
is “a string valued property,” not a method. That means we need to use the get
keyword in our password class (or Object.defineProperty
on an instance) to declare this property.
Now when we call toString
on our password, we get [object Password]
. Although this is not something many apps would need, it can be useful from a debugging and logging perspective.
Symbol.hasInstance
Another less common, but still powerful Symbol is Symbol.hasInstance
. It controls the behavior of the instanceof
operator.
Fortunately it is not possible to redefine native implementations of the Symbol.hasInstance
function like Function.prototype[Symbol.hasInstance]
. However, it is possible to do so on custom classes and objects like Password
.
The main use for Symbol.hasInstance
is in libraries that want more control over instanceof
checks. For example, GraphQL JS uses Symbol.hasInstance
to check for specific symbols that identify GraphQLTypes.
Symbol.iterator
Symbol.iterator
is among the most well-known and useful symbols. It enables user-defined iterables that implement the iteration protocol.
Here is an example of Symbol.iterator
's power. They can be used to extend or override native object’s prototype. That means with just a few lines, we can define iteration for Objects globally.
Whether you should is a different question.
With this code, Objects can be spread into Arrays of [key, value]
pairs. Likewise, Objects can be iterated using for...of
loops.
In the context of our Password
class, we don’t even have to write our own iterator! Since a Password can be coerced into a String, we can use the native String iterator directly.
Doing so allows us to spread or iterate a Password as a sequence of asterisks just as we could with a String.
Putting it all together, our final Password
class looks like:
And has some interesting properties:
Future Symbols
The result is a class that masks the original password, is comparable, iterable, and convertible as we might expect from a String. While I have introduced five well-known Symbols, at the time of writing (May 2019) there are actually thirteen! The full list includes:
- Symbol.asyncIterator
- Symbol.hasInstance
- Symbol.isConcatSpreadable
- Symbol.iterator
- Symbol.match
- Symbol.matchAll
- Symbol.replace
- Symbol.search
- Symbol.species
- Symbol.split
- Symbol.toPrimitive
- Symbol.toStringTag
- Symbol.unscopables
Be on the lookout for more soon! There are several suggestions and proposals for Private Symbols, Symbol.thenable
, Symbol.isAbstractEquals
, Symbol.equals
, Symbol.inspect
, Symbol.inObject
, and more.
Symbols are a unique, immutable data type unlike their counterparts in other languages. As the Javascript language continues to evolve, I expect Symbols to become a mainstay in Javascript metaprogramming.
If you found this helpful, please give some claps 👏 👏 👏