The symbol data type: syntax and use cases

Reading Time: 4 minutes 🕑

Last updated: October 7, 2021.

Symbol is one of seven primitive data types in JavaScript:

  • Number
  • String
  • Boolean
  • Bigint
  • Null
  • Undefined
  • Symbol

Primitive data types are the most basic possible data units in JavaScript, i.e. they cannot be reduced to simpler data.

So when symbol was added to the language with ES6, it joined a pretty exclusive club.

So what is the symbol type? And when should we use it?

What is symbol?

A new symbol data type can be created by calling Symbol().

Symbol is an in-built object and calling it returns a new instance of the symbol data type.

Each time a new symbol data type is created, a unique identification code is created that is guaranteed to be unique for that symbol. So no two symbol codes will ever clash.

However, we cannot access or edit the identification code. We only need to know that it exists and is unique.

In fact, this is an intended feature of symbol: by preventing us from intentionally or unintentionally changing its underlying identification code, we can be certain that it value is unique (unlike a readable id, which can be edited and overwritten).

Even though we cannot access the identification code directly, we can prove this by comparing two instances of the symbol data type, created by calling Symbol():

const newSymbol = Symbol();

console.log(typeof newSymbol) // Symbol
console.log(newSymbol) // Undefined

const secondSymbol = Symbol()

newSymbol == secondSymbol 
// false (because of difference in identification codes)
newSymbol === secondSymbol 
// false

As an optional parameter, we can add a reference for a new symbol.

This is not the same as setting the underlying identification code. It is only for our reference.

We can see that we have not changed the identification code if we set the same reference for two symbols:

const newSymbol = Symbol('Fitzgerald');
const secondSymbol = Symbol('Fitzgerald');

newSymbol == secondSymbol // false
newSymbol === secondSymbol // false

This is because they are still different: each has its own, unique underlying code.

Usage

#1 Preventing key clashes in objects

The symbol type is useful when we are dealing with stored data and want to ensure that no two entries clash.

For example,

const userData = {
   "Stan": { age: 23, score: 68 },
   "Tina": { age: 22, score: 75 },
   "Celine": { age: 20, score: 67 },
   "Stan": { age: 24, score: 89 },
}

If I now call console.log(userData), it returns data for Tina, Celine and the second Stan only because there is a clash in the entry keys (the first “Stan” entry is overwritten):

Console log output

However, if we make the keys symbols, there will not be a clash: the identification codes of each symbol will be unique:

const userData = {
[Symbol("Stan")] : { age: 23, score: 68 },
[Symbol("Tina")] : { age: 22, score: 75 },
[Symbol("Celine")] : { age: 20, score: 67 },
[Symbol("Stan")] : { age: 24, score: 89 },
}

Now, if I log the contents of the object to the console, both “Stan” entries will be printed:

Console log output for object with clashing string keys

#2 For storing private information

A feature of symbols is that they cannot be iterated over in the normal way. This can be useful if we want to exclude private data from normal iterating operations (e.g. not printing certain entries).

For example, let’s change userData so now some entry keys are strings and others symbols and try looping over them:

const userData = {
"Stan" : { age: 23, score: 68 },
"Tina" : { age: 22, score: 75 },
[Symbol("Celine")] : { age: 20, score: 67 },
[Symbol("Stan")] : { age: 24, score: 89 },
}

for (person in userData) {
    console.log(person, userData[person])
}

Result:

Console log for a 'for in' loop with symbol and non-symbol key types

Only the non-symbol entries were printed. The symbol entries are excluded from the looping operation.

Can data be accessed by its symbol value?

By design, we cannot access and edit the identification code underlying a symbol. And it is excluded from normal iterating operations.

But we can access data by its underlying symbol value if we really want to.

First, we use the getOwnPropertySymbols()method on the in-built JavaScript Object to return symbol values. This returns the symbol values themselves.

To get the stored data corresponding to each returned symbol, we can map through each symbol, and get the corresponding entry for each from userData:

const userData = {
"Stan" : { age: 23, score: 68 },
"Tina" : { age: 22, score: 75 },
[Symbol("Celine")] : { age: 20, score: 67 },
[Symbol("Stan")] : { age: 24, score: 89 },
}

const symbols = Object.getOwnPropertySymbols(userData);

console.log(symbols);
// Symbols only!

const data = symbols.map((symbol) => userData[symbol])

console.log(data);
// The data

The result:

Console log when mapping by symbol value to get object properties

Only the two entries stored with the symbol entry keys are returned.

So using this long method, we can still access entries stored with a symbol key.

Summary

The symbol type is useful for two purposes:

  • Ensuring no clash of key values for properties of objects.
  • Excluding entries in normal looping operations.

You can find out more about ES6 features and later updates to the JavaScript language in our ES6+ section.