A Symbol is a new primitive type introduced in ES6.

At first glance, it may seem like a symbol is just a unique string. However, there are some important differences between symbols and strings.

In this post, we’ll look at what symbols are, learn about their features and discuss some use cases for them.

banner

Symbols as Unique identifiers

To start off, let’s look at how things were before Symbols were introduced.

Suppose we had a variable called myId that we wanted to use as a unique identifier within our application. We could use a string for this purpose:

const myId = "id"

However, the problem with this approach is that there is nothing stopping another developer from using the same string as an identifier for another object:

const otherId = "id"

console.log(id === other_id) // true

// do something really important based on the id
function processId(id) {
  // ...
}

This is a problem, since there is no way to distinguish between the two variables.

We could solve this by using a unique string for each identifier, but generating unique strings comes with its own set of problems, and besides, a random string like 5d63411a is not very readable.

Symbols solve this problem by providing a way to create identifiers that are guaranteed to be unique.

const myId = Symbol("id")
const otherId = Symbol("id")

console.log(myId === otherId) // false

Also, Symbols are pretty readable, since they can be given a description when they are created:

console.log(myId)
// Output: 
// Symbol(id)

Using Symbols as Object Keys

The same problem of uniqueness applies when using strings as object keys.

For example, if you want to create an object and you want to prevent other parts of the code from accidentally overwriting the keys, using a Symbol as the key is a good idea:

const myKey = Symbol("key")

const obj = {
  [myKey]: "value",
  "someOtherKey": "some other value"
}

The only way to access the value of obj[myKey] is to use the myKey variable itself:

console.log(obj[myKey]) 
// Output:
// value

One unique feature of Symbols is that they are not enumerable. This means that they will not show up when you iterate over the keys of an object:

console.log(Object.keys(obj))
// Output:
// ["someOtherKey"]

This is useful when you want to hide certain properties of an object from other parts of the code.

For example, if you want to create a library that exposes an object to the user, but you don’t want the user to be able to access certain properties of the object, you can use Symbols to hide those properties.

Note that you can still access the value of a Symbol key using the Object.getOwnPropertySymbols method:

console.log(Object.getOwnPropertySymbols(obj))
// Output:
// [Symbol(key)]

So technically, the value of a Symbol key is not completely hidden, but it is not easily accessible either.

The Symbol.for Method - Creating Global Symbols

If you want to create a Symbol that is accessible from anywhere in your code, you can use the Symbol.for method:

const mySymbol = Symbol.for("myKey")

This creates a Symbol in the global Symbol registry. If there is already a Symbol with the same key, it will return that Symbol instead of creating a new one:

symbol global registry

You can think of it as a global map of keys to Symbols. This is useful when you want to create a Symbol that is accessible from anywhere in your code.

However, note that you will lose the uniqueness of the Symbol if you use the same key to create a Symbol in another part of your code:

const mySymbol = Symbol.for("myKey")

// this will return the same Symbol as above
const myOtherSymbol = Symbol.for("myKey")

console.log(mySymbol === myOtherSymbol) // true

Well Known Symbols

There are a few Symbol values that are built into the language. These are called “well known symbols”.

But why do we need these? Remember when we said that Symbols can be used to uniquely identify keys in an object? Well, these well known symbols are used to define the behavior of certain core objects in the language.

There are a number of well known symbol properties, but let’s take the Symbol.toPrimitive symbol for example. This symbol is used to define how an object should be converted to a primitive value:

const obj = {
  [Symbol.toPrimitive]: function(hint) {
    if (hint === "string") {
      return "hello"
    } else if (hint === "number") {
      return 123
    } 
  }
}

console.log(String(obj)) // hello
console.log(Number(obj)) // 123

Before well known symbols were introduced, this kind of overloading was possible using regular object keys, such as obj.toString or obj.valueOf.

However, this approach was not very clean, since it required us to use a string as the key, which could potentially conflict with other parts of the code.

Well known symbols solve this problem by providing a way to define the behavior of core objects in the language without having to worry about conflicts with other parts of the code, or breaking changes in future versions of the language.

Conclusion

In this post, we looked at what Symbols are, learned about their features and discussed some use cases for them.

Symbols create native values that are guaranteed not to collide with one another. So if you want to create an identifier that is unique within your application, you should use a Symbol instead of a string.

However, note that Symbols are not completely unique. If you use the same key to create a Symbol using Symbol.for in another part of your code, you will lose the uniqueness of the Symbol.

If you want to know more about Symbols, check out the MDN docs.