Please support this book: buy it or donate

20. Symbols



Symbols are primitive values that are created via the factory function Symbol():

  1. const mySymbol = Symbol('mySymbol');

The parameter is optional and provides a description, which is mainly useful for debugging.

On one hand, symbols are like objects in that each value created by Symbol() is unique and not compared by value:

  1. > Symbol() === Symbol()
  2. false

On the other hand, they also behave like primitive values – they have to be categorized via typeof and they can be property keys in objects:

  1. const sym = Symbol();
  2. assert.equal(typeof sym, 'symbol');
  3. const obj = {
  4. [sym]: 123,
  5. };

20.1. Use cases for symbols

The main use cases for symbols are:

  • Defining constants for the values of an enumerated type.
  • Creating unique property keys.

20.1.1. Symbols: enum-style values

Let’s assume you want to create constants representing the colors red, orange, yellow, green, blue and violet. One simple way of doing so would be to use strings:

  1. const COLOR_BLUE = 'Blue';

On the plus side, logging that constant produces helpful output. On the minus side, there is a risk of mistaking an unrelated value for a color, because two strings with the same content are considered equal:

  1. const MOOD_BLUE = 'Blue';
  2. assert.equal(COLOR_BLUE, MOOD_BLUE);

We can fix that problem via a symbol:

  1. const COLOR_BLUE = Symbol('Blue');
  2. const MOOD_BLUE = 'Blue';
  3. assert.notEqual(COLOR_BLUE, MOOD_BLUE);

Let’s use symbol-valued constants to implement a function:

  1. const COLOR_RED = Symbol('Red');
  2. const COLOR_ORANGE = Symbol('Orange');
  3. const COLOR_YELLOW = Symbol('Yellow');
  4. const COLOR_GREEN = Symbol('Green');
  5. const COLOR_BLUE = Symbol('Blue');
  6. const COLOR_VIOLET = Symbol('Violet');
  7. function getComplement(color) {
  8. switch (color) {
  9. case COLOR_RED:
  10. return COLOR_GREEN;
  11. case COLOR_ORANGE:
  12. return COLOR_BLUE;
  13. case COLOR_YELLOW:
  14. return COLOR_VIOLET;
  15. case COLOR_GREEN:
  16. return COLOR_RED;
  17. case COLOR_BLUE:
  18. return COLOR_ORANGE;
  19. case COLOR_VIOLET:
  20. return COLOR_YELLOW;
  21. default:
  22. throw new Exception('Unknown color: '+color);
  23. }
  24. }
  25. assert.equal(getComplement(COLOR_YELLOW), COLOR_VIOLET);

Notably, this function throws an exception if you call it with 'Blue':

  1. assert.throws(() => getComplement('Blue'));

20.1.2. Symbols: unique property keys

The keys of properties (fields) in objects are used at two levels:

  • The program operates at a base level. Its keys reflect the problem domain.

  • Libraries and ECMAScript operate at a meta-level. For example, .toString is a meta-level property key:

The following code demonstrates the difference:

  1. const point = {
  2. x: 7,
  3. y: 4,
  4. toString() {
  5. return `(${this.x}, ${this.y})`;
  6. },
  7. };
  8. assert.equal(String(point), '(7, 4)');

Properties .x and .y exist at the base level. They are the coordinates of the point encoded by point and reflect the problem domain. Method .toString() is a meta-level property. It tells JavaScript how to stringify this object.

The meta-level and the base level must never clash, which becomes harder when introducing new mechanisms later in the life of a programming language.

Symbols can be used as property keys and solve this problem: Each symbol is unique and never clashes with any string or any other symbol.

As an example, let’s assume we are writing a library that treats objects differently if they implement a special method. This is what defining a property key for such a method and implementing it for an object would look like:

  1. const specialMethod = Symbol('specialMethod');
  2. const obj = {
  3. [specialMethod](x) {
  4. return x + x;
  5. }
  6. };
  7. assert.equal(obj[specialMethod]('abc'), 'abcabc');

This syntax is explained in more detail in the chapter on objects.

20.2. Publicly known symbols

Symbols that play special roles within ECMAScript are called publicly known symbols. Examples include:

  • Symbol.iterator: makes an object iterable. It’s the key of a method that returns an iterator. Iteration is explained in its own chapter.

  • Symbol.hasInstance: customizes how instanceof works. It’s the key of a method to be implemented by the right-hand side of that operator. For example:

  1. class PrimitiveNull {
  2. static [Symbol.hasInstance](x) {
  3. return x === null;
  4. }
  5. }
  6. assert.equal(null instanceof PrimitiveNull, true);
  • Symbol.toStringTag: influences the default .toString() method.
  1. > String({})
  2. '[object Object]'
  3. > String({ [Symbol.toStringTag]: 'is no money' })
  4. '[object is no money]'

Note: It’s usually better to override .toString().

20.3. Converting symbols

What happens if we convert a symbol sym to another primitive type? Tbl. 17 has the answers.

Table 17: The results of converting symbols to other primitive types.
Convert toExplicit conversionCoercion (implicit conv.)
booleanBoolean(sym) OK!sym OK
numberNumber(sym) TypeErrorsym*2 TypeError
stringString(sym) OK''+sym TypeError
sym.toString() OK${sym} TypeError

One key pitfall with symbols is how often exceptions are thrown when converting them to something else. What is the thinking behind that? First, conversion to number never makes sense and should be warned about. Second, converting a symbol to a string is indeed useful for diagnostic output. But it also makes sense to warn about accidentally turning a symbol into a string property key:

  1. const obj = {};
  2. const sym = Symbol();
  3. assert.throws(
  4. () => { obj['__'+sym+'__'] = true },
  5. { message: 'Cannot convert a Symbol value to a string' });

The downside is that the exceptions make working with symbols more complicated. You have to explicitly convert symbols when assembling strings via the plus operator:

  1. > const mySymbol = Symbol('mySymbol');
  2. > 'Symbol I used: ' + mySymbol
  3. TypeError: Cannot convert a Symbol value to a string
  4. > 'Symbol I used: ' + String(mySymbol)
  5. 'Symbol I used: Symbol(mySymbol)'

20.4. Further reading

  • For in-depth information on symbols (cross-realm symbols, all publicly known symbols, etc.), see “Exploring ES6”