Symbols are a new, unique, primitive type introduced in ECMAScript 6 (ES6). They were added to the language in order to solve the problem of extending the functionality of Object
while maintaining backwards-compatibility with code written in earlier versions of JavaScript. With their addition comes the ability for developers to affect the behavior of the language in new and interesting ways. This article will introduce the concept of a symbol, explain their purpose, and show a couple of the most common ways symbols can be used today.
Before getting into any details, let’s look at how to create a new symbol.
const mySymbol = Symbol()
The symbol primitive is created using a built-in factory function. Factories are used throughout JavaScript to build native elements. For instance, creating a new DOM element is done through document.createElement()
. Symbols created in this manner are called “custom symbols.”
The Symbol
object also has a number of named symbols provided by JavaScript called “well-known symbols.” These symbols are known by JavaScript and are used to interact with and modify the behavior of the underlying JavaScript code. The most common of the well-known symbols is Symbol.iterator
, which is used to add a means of iterating over an object’s values. There are also symbols that allow objects to be used like regular expressions, change how types are reported to JavaScript (e.g. via typeof
), and how to construct and convert custom objects.
With a new, unique, primitive type available, JavaScript can use symbols as property names alongside strings to add functionality to objects without worrying about colliding with previously used names. This is important, because if the ECMAScript specification chose to use the string “iterator” instead of Symbol.iterator
then any older code that may have used the string “iterator” as a property name could have unintended consequences when used in an ECMAScript 6 context.
Symbols solve the problem of name collisions. Because they are unique, no two symbols will ever share the same value.
Symbol() !== Symbol()
This means that JavaScript can continue to add functionality by adding well-known symbols without ever colliding with previously written code.
What can Symbols do?
Symbols allow developers to extend and affect the behavior of the JavaScript engine. For example, by using the well-known symbol, Symbol.iterator
(also referred to as @@iterator
), an object can describe what data is provided when it is used with for…of or a spread operator.
class Data {
constructor(... rest) {
this._data = rest;
}
*[Symbol.iterator]() {
for (let item of this._data) {
yield item;
}
}
}
console.log([... new Data(1, 2, 3)]); // outputs: [ 1, 2, 3 ]
In this example the JavaScript engine looks at the custom object for an implementation at @@iterator
and uses it as part of its internal routines to produce values used for constructing the array literal.
This change of behavior is even more obvious when working with symbols that change how JavaScript works with staples of the language. For instance, @@toStringTag
is a symbol that lets you define how your object is described.
let arraylike = {
0: 'zero',
1: 'one',
length: 2,
[Symbol.toStringTag]: 'Array',
*[Symbol.iterator]() {
for (let i = 0; i < this.length; i++) {
yield this[i];
}
}
}
console.log(arraylike.toString()); // [object Array]
Or @@replace allows an object to behave in a context previously reserved for regular expressions.
const Summary = {
maxLength: 35,
defaultEnding: '...',
[Symbol.replace](str, replacement) {
if (str.length > this.maxLength) {
replacement = replacement || this.defaultEnding;
const length = this.maxLength - replacement.length;
return str.substring(0, length) + replacement;
}
return str;
}
};
const sentence =
'This sentence is way too long and will be summarized!';
// prints: This sentence is way too long an...
console.log(sentence.replace(Summary));
Giving developers the ability to override language-level features that have previously only been accessible to the browser is a key component in the Extensible Web Manifesto. It allows for more experimentation and lets developers build libraries around low-level features that were previously out of reach.
Using Symbols
Symbols are available natively in all modern browsers starting with Edge 12. However, except for Symbol.iterator
, many of the well-known symbols have not been implemented across all browsers. This means that even in modern browsers with symbol support, many of the well-known symbols will need to be polyfilled. It is important to note that a polyfill of some well-known symbols cannot replicate behaviors that come from syntax or operators, like how @@hasInstance
interacts with instanceof
or @@toPrimitive
is used to type cast an object to a value.
For ES5 browsers without a native symbol type, there are additional considerations. For one, since symbols are primitive types a polyfill cannot correctly replicate typeof checks and Symbol()
will return a string. Also, the in
operator will return polyfilled symbols because underneath they are just strings. And finally, Symbol.for()
and Symbol.keyFor
does not provide cross-realm support.
For TypeScript users the situation becomes more complicated. While TypeScript has support for symbols when emitting ES6 code, it does not provide symbol typings when down-emitting to ES5 and will report symbol usage as an error. TypeScript developers can resolve this in one of three ways:
- Use dojo/core or another library with shims to support ES6 features with ES5 syntax
- Create a build chain that transpiles TypeScript to ES6, then uses Babel to transpile to ES5
- Use
any
types in place of asymbol
type
Each solution has trade-offs that must be made to support symbols in an ES5 environment. We believe using a shim library to wrap ES6 functionality in an ES5 syntax offers the best way to maintain types and consistency while targeting multiple platforms. There are also plans to help address these issues through conditional compilation and granular targeting features planned in the TypeScript roadmap.
These caveats may sound onerous, but many of them can be resolved using a transpiler like Babel, a good polyfill like core.js, and being aware of the exceptions listed above. With these tools in place, engineers can take advantage of the modern concepts and syntax provided by ECMAScript 6 and begin writing more efficient and easier-to-read code.
Learning more
We cover Symbols and many other useful additions to ES6 in our ES6 & TypeScript for the Enterprise Developer workshops. We believe it’s more important than ever to learn the fundamentals of ES6 and TypeScript. With the first substantial changes to the language in nearly 20 years, now is the time to learn how to efficiently leverage these changes to our primary language for creating web applications.
We also provide help when learning ES6 and TypeScript through any of our JavaScript support plans. And with the many changes to the language, there are several ways to improve the approach to the architecture of your application. Contact us to discuss your application architecture, and to learn more about how we can help!