loading...
Cover image for JavaScript ES6 Symbols

JavaScript ES6 Symbols

coleturner profile image Cole Turner Updated on ・3 min read

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.

Example

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?

Shared Symbols

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.

Why use Symbols?

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

Symbols can be used for metaprogramming

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.

Symbols are separate from string keys

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.

Symbols can be used as ENUM

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

**JavaScript Symbols - ENUM example

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.

JavaScript Metaprogramming with Symbols

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.

Symbol.asyncIterator

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
}

Symbol.hasInstance

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;
}

Symbol.iterator

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)];

Conclusion

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.

Posted on by:

coleturner profile

Cole Turner

@coleturner

Cole Turner is a senior software engineer, based in the Bay Area (CA), who specializes in: developing web application products, seamless user experience, and cross-functional communications.

Discussion

markdown guide
 

Great work Cole.

A few uses of Symbols there that I wasn't aware of.