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

Melt of the Day: Brisket, Muenster, & Grilled Onions

2 min read

You can't go wrong with brisket. This recipe uses Snow's BBQ, Texas Monthly's #1 Pick, and grilled onions for the perfect brisket and grilled cheese melt.

Lessons Learned in Freelancing

12 min read

If you want to become a freelance web developer, these are the lessons that will help you have a great time working with clients and being effective.

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.