DEV Community

Marko Marinovic for Blank

Posted on • Edited on

JavaScript magic with Symbols

Q: How to implement object magic which has the following behavior?

const magic = {};

console.log(2 + +magic); // 42
console.log(5 + magic); // 1337
console.log(`JavaScript is ${magic}`) // "JavaScript is awesome"
console.log(magic.toString()); // "[object magic]"
Enter fullscreen mode Exit fullscreen mode

The question is very interesting and you are probably thinking "what kind of sorcery is this 😱?". To solve this mystery we need to learn about Symbols in JavaScript and see how they can help us in this case.

Symbols in JavaScript

A symbol is a primitive data type introduced in ES6. It's created with Symbol function and globally unique. Symbols can be used as object properties to provide uniqueness level access to objects and as hooks into built-in operators and methods, enabling us to alter the default behavior of JavaScript.

const mySymbol = Symbol('mySymbol');
typeof mySymbol // "symbol"

Symbol('mySymbol') === Symbol('mySymbol') // false
Enter fullscreen mode Exit fullscreen mode

Symbols as object properties

Since symbols are globally unique, they can be used in a situation where there is a risk of property name collision. Imagine you are working on a library and need to attach your lib metadata to the supplied object.

const magic = {};

function someLibFunction(obj){
   obj.meta = 'MyLibMeta';
}

someLibFunction(magic);

console.log(magic); // { meta: 'MyLibMeta' }
Enter fullscreen mode Exit fullscreen mode

There is a problem with this code because the meta property could be overwritten by the user code or other library.

const magic = {};

function someLibFunction(obj){
   obj.meta = 'MyLibMeta';
}

function userFunction(obj){
   obj.meta = 'I use this for my code';
}

someLibFunction(magic);
userFunction(magic);

console.log(magic); // { meta: 'I use this for my code' }
Enter fullscreen mode Exit fullscreen mode

Now, userFunction has overwritten the meta property and lib is not working properly. Lib writers can use symbols for property names to avoid name collisions with other code.

const magic = {};

const libMetaSymbol = Symbol('meta');

function someLibFunction(obj){
   obj[libMetaSymbol] = 'MyLibMeta';
}

function userFunction(obj){
   obj.meta = 'I use this for my code';
}

someLibFunction(magic);
userFunction(magic);

console.log(magic[libMetaSymbol]); // 'MyLibMeta'
console.log(magic.meta); // 'I use this for my code'
Enter fullscreen mode Exit fullscreen mode

Symbols as properties are not available through Object.keys, but rather through Reflect.ownKeys. This is for the purpose of backward compatibility because the old code doesn't know about symbols.
Keep in mind that Reflect.ownKeys returns all property names and symbols. If you need to read only symbols, use Object.getOwnPropertySymbols().

const magic = { id: 1 };
const metaSymbol = Symbol('meta');

magic[metaSymbol] = 'MyMeta';

console.log(Object.keys(magic)); // ["id"]
console.log(Reflect.ownKeys(magic)); // ["id", [object Symbol] { ... }]
console.log(Object.getOwnPropertySymbols(magic)); // [[object Symbol] { ... }]
Enter fullscreen mode Exit fullscreen mode

Well-known symbols

Well-known symbols are defined as static properties on Symbol object.
They are used by built-in JavaScript functions and statements such as toString() and for...of. toString() method uses Symbol.toStringTag and for...if uses Symbol.iterator. There are many more built-in symbols and you can read about them here.

To solve the magic object question, we need to look closer at Symbol.toPrimitive and Symbol.toStringTag symbols.

Symbol.toPrimitive

JavaScript calls the Symbol.toPrimitive method to convert an object to a primitive value. The method accepts hint as an argument, hinting at what kind of conversion should occur. hint can have a value of string, number, or default. There is no boolean hint since all objects are true in boolean context.

Symbol.toStringTag

Property used internally by Object.prototype.toString() method. You would assume that string template literals also call Symbol.toStringTag under the hood, but that's not the case. Template literals call Symbol.toPrimitive method with a string hint.

Answering the question

Now when we know a lot about symbols, let's see the answer to the magic object question.

const magic = {
  [Symbol.toPrimitive](hint) {
    if (hint == 'number') {
      return 40;
    }
    if (hint == 'string') {
      return 'awesome';
    }
    return 1332;
  },

  get [Symbol.toStringTag]() {
    return 'sorcery';
  }
}

console.log(2 + +magic); // 42
console.log(5 + magic); // 1337
console.log(`JavaScript is ${magic}`) // "JavaScript is awesome"
console.log(magic.toString()); // "[object sorcery]"
Enter fullscreen mode Exit fullscreen mode

First console.log converts magic to a number and adds 2. Conversion to number internally calls Symbol.toPrimitive function with hint number.

Second console.log adds magic to 5. Addition internally calls Symbol.toPrimitive function with hint default.

Third console.log uses magic with string template literals. Conversion to string, in this case, calls Symbol.toPrimitive function with hint string.

Final console.log calls toString() method on magic object. toString() internaly calls Symbol.toStringTag property.

Conclusion

Symbols are globally unique primitive types which allow us to avoid property name collision and hook into JavaScript internals. If you want to read more about symbols, visit EcmaScript specs and Mozzila docs.

Do you find symbols to be useful in your everyday programming?

Top comments (1)

Collapse
 
leorolland profile image
Léo • Edited

Nice article thank you :) Good job