JavaScript ES6 Symbols: A Metaprogramming Primitive

Cole Turner
Cole Turner
2 min read
Cole Turner
JavaScript ES6 Symbols: A Metaprogramming Primitive
Blog
 
LIKE
 
LOVE
 
WOW
 
LOL

The JavaScript Symbol is a primitive data structure that has a unique value. They can be used as identifiers since no two symbols are the same. Unlike strings, Symbols can be used to create properties that don't overlap with other libraries or modules.

const sym = Symbol();
const namedSymbol = Symbol('javascript');

sym === namedSymbol // false
typeof sym // "symbol"

console.log(namedSymbol); // Symbol(javascript)
console.log(namedSymbol.description); // javascript

Pretty neat, now our JavaScript app can uniquely identify properties without any risk of colliding with another identifier. But what if we want to share symbols across the codebase?

const sym1 = Symbol.for('javascript');
const sym2 = Symbol.for('javascript');

sym1 === sym2 // true

When we use Symbol.for we can leverage shared symbols that are available in the global symbol registry for our codebase.

Now that we understand that Symbols are unique identifiers, we can understand the potential for what a software engineer can do with them.

const UserType = Symbol('userType');
const Administrator = Symbol('administrator');
const Guest = Symbol('guest');

const currentUser = {
  [UserType]: Administrator,
  id: 1,
  name: "Cole Turner"
};

console.log(currentUser); // {id: 1, name: "Cole Turner", Symbol(userType): Symbol(administrator)}

console.log(JSON.stringify(currentUser)); // {"id":1,"name":"Cole Turner"}

currentUser[UserType] == Administrator; // true
currentUser[UserType] == Guest; // false

In the example above, a symbol is used to type the object. The property is only available when referenced through the symbol reflection. This is great for when we want to add properties to an object that we don't want to appear in non-symbol reflection, such as JSON formatting or object iteration.

const languages = {
  javascript: 'JavaScript';
};

// Extend an object without conflict
const isLocal = Symbol('local');
const localLanguages = {
  ...languages,
  [isLocal]: true
};

// Detect if we're using local or the original languages object
[languages, localLanguages].map(obj => {
  if (obj[isLocal]) {
    console.log('Local languages:', obj);
  } else {
    console.log('Original languages:', obj);
  }
});

In the example above, we can extend objects without conflict with their original properties. This also means that when we are stringifying, the symbols are not included.

A great use case for Symbols is when there is a need for enumerated values.

const Tree = Symbol('🌴');
const Flower = Symbol('🌻');
const Leaf = Symbol('🍁');
const Mushroom = Symbol('🍄');

const plantTypes = [Tree, Flower, Leaf, Mushroom];

function createPlant(type) {
  if (!plantTypes.includes(type)) {
    throw new Error('Invalid plant type!');
  }
}

Here we are using Symbols to control behaviors without those properties leaking into the typical reflection, and preventing runtime errors from typos.

With Symbols we can dive deep into low-level JavaScript to change behaviors for various use cases. This lets us create powerful objects that can do more than meets the eye. Here are some examples of how we can use Symbols for JavaScript metaprogramming.

const tenIntegers = {
  async* [Symbol.asyncIterator]() {
    for (let i = 1; i <= 10; i++) {
      yield i;
    }
  }
}

for await (const i of tenIntegers) {
  console.log(i);
  //  1
  //  ...
  //  10
}

const Loggable = Symbol('loggable');

class LoggableError extends Error {
  static [Symbol.hasInstance](instance) {
    return instance instanceof LoggableError || instance[Loggable] === true;
  }
}

class ApplicationError extends Error {
  [Loggable] = true;

  logError() {
    if (this instanceof LoggableError) {
      return;
    }

    fetch('/log', { message: this.message });
  }
}

class DatabaseError extends ApplicationError {
    [Loggable] = false;
}

const users = {
  1: { name: 'Cole Turner' },
  2: { name: 'Anonymous' },
};

function toValuesArray(obj) {
  return {
    ...obj,

    [Symbol.iterator]: function* () {
      for (const value of Object.values(this)) {
        yield value;
      }
    },
  };
}

// toValuesArray will now change the spread of our object
const arrayOfUsers = [...toValuesArray(users)];

The Symbol is a new primitive that can unlock a lot of potential with metaprogramming in JavaScript. They make great enumerated values, allow software engineers to extend objects without collision, and can separate concerns when working with data across the codebase.

For more information, check out the MDN documentation on Symbols.

 
LIKE
 
LOVE
 
WOW
 
LOL

Related Tags

JavaScript

More Stories

Why I Don't Like Take-Home Challenges

4 min read

If you want to work in tech, there's a chance you will encounter a take-home challenge at some point in your career. A take-home challenge is a project that you will build in your free time. In this post, I explain why I don't like take-home challenges.

Standing Out LOUDER in the Technical Interview

5 min read

If you want to stand out in the technical interview, you need to demonstrate not only your technical skills but also your communication and collaboration skills. You need to think LOUDER.

See more posts

Read it before anyone else. Subscribe to my newsletter for early access to the latest news in software engineering, web development, and more.