javaWhen I first read about Symbols coming to JavaScript, I couldn’t figure out what the point was or even what they were, so I just kind of parked it. Lately I’ve been doing some work with Async Iterators, and it all fell into place. If you’re still digging into V8 for Apps Script you may not have come across Symbols, or like me, you didn’t see the point of them, let’s see if this article can shed some light and explain what Symbols are all about.

What is a Symbol ?

It’s a new type of primitive (like string, number etc) but it has some odd characteristics.

  • Every time you create a Symbol it has a unique value.
  • You can’t convert to a string and log it out or export it – so it’s a thing you use inside your App
  • There are some ‘well known’ Symbols which are built in to JavaScript with special meaning
  • At first it’s hard to imagine what you would need them for

Symbols are unique

Every time you create a Symbol, it’s different to any other Symbol

  // a symbol is always unique
const s1 = Symbol()
const s2 = Symbol()
console.log(s1 === s2) // false
each symbol is unique

You can label a Symbol

Although you can assign a label to a symbol, that label has no effect on its value

  // how about if i label them
const s3 = Symbol('s3')
const s4 = Symbol('s4')
console.log(s3 === s4) //false

// how about if i label them the same thing
const s5 = Symbol('s5and6')
const s6 = Symbol('s5and6')
console.log(s5 === s6) //false
labelling symbols

Labels are for debugging

The point of labels is so you can inspect a label you may have attached to a symbol for debugging purposes. typeof Symbol just returns ‘symbol’. The .toString() method just returns ‘Symbol’, or if you’ve given it a label  ‘Symbol(label)’

  // we cant print it
console.log(s5) // {}
// it has a special new primitive type
console.log(typeof s5) // symbol

// so what;s the point of the label?
console.log(s3.toString(), s4.toString(), s5.toString(), s6.toString())
// Symbol(s3) Symbol(s4) Symbol(s5and6) Symbol(s5and6)

So what’s the point of Symbols

So far, they seem pretty pointless right ? A thing that we can create but can’t then inpect what it is we’ve created. But wait…

Safe property names

Let’s say you get an array of object from an API that looks like this

  const apiData = [{
color: 'red',
id: 1
}, {
color: 'blue',
id: 1
}]
array of objects

You want to enhance that object with a property of your own – say ‘id’. You don’t know if that object already has an id field, (or may have in the future), so you can’t just add a field called ‘id’. You could make up an unlikely/unique property name, but then it’ll get messy elsewhere in your app. Instead you can use a Symbol as a property name, ,with confidence that it won’t conflict with any other property name already in the object, or that you may add elsewhere.  Here’s I’m using a Symbol and adding a debug label of ‘id’

  const sid = Symbol('id')
const data = apiData.map((d, i) => ({
...d,
[sid]: i
}))
use a symbol as a property name

Property missing when logged

But when I log that enhanced object, the property with the Symbol for ‘id’ is missing

  // but when i log it - nothing there
console.log(data) // [ { color: 'red', id: 1 }, { color: 'blue', id: 1 } ]
symbol not logged

Property missing when Stringified

When the object is stringified, it’s missing too

  // so whats happened to my id - can I stringify it? - nope
console.log(JSON.stringify(data)) // [{"color":"red","id":1},{"color":"blue","id":1}]
missing when stringified

Property missing from Object.keys

It’s missing from this too


console.log(data.map(Object.keys)) // [ [ 'color', 'id' ], [ 'color', 'id' ] ]
how about Object.keys – nope

Hidden properties

This seems odd at first, but it is by design. Code prior to Es6 wouldn’t have known about symbols so enhancing them now might break things. Also since Symbol properties only exist inside your app, there’s not much point in stringifying them, as the value of a Symbol is non exportable and of no use externally anyway.

This is the concept of ‘hidden properties’ – only visible if you know how to look for them (but see later), and only inside your app (but also see later)

  // [ 0, 1 ]
console.log(data.map(d => d[sid]))
it's only accessible if you know the symbol

Spreading

Making a copy of an object by spreading retains the hidden properties

  console.log(data.map(d => ({ ...d })).map(d => d[sid])) // [ 0, 1 ]
how about spreading it .. yep it's there

Reflect

Reflect methods are an Es6 improvement on many of the Object methods, and Reflect can see Symbol properties

 console.log(data.map(Reflect.ownKeys)) // [ [ 'color', 'id', {} ], [ 'color', 'id', {} ] ]
Reflect.ownKeys sees Symbol Properties

Object

Although most Object methods don’t see Symbol, properties there’s a new method to see just Symbol properties

 console.log(data.map(Object.getOwnPropertySymbols)) // [ [ {} ], [ {} ] ]
Object getOwnPropertySymbols

Getting all the properties and values

Now we can get all the properties using Reflect.ownKeys

  // [ [ [ 'color', 'red' ], [ 'id', 1 ], [ 'Symbol(id)', 0 ] ],
// [ [ 'color', 'blue' ], [ 'id', 1 ], [ 'Symbol(id)', 1 ] ] ]
console.log(data.map(d => Reflect.ownKeys(d).map(k => [k.toString(), d[k]])))
get all the keys with reflect

Or just the ones that are Symbols

using Object.getOwnPropertySymbols

 console.log(data.map(d => Object.getOwnPropertySymbols(d).map(k => [k.toString(), d[k]]))) // [ [ {} ], [ {} ] ]
Object getOwnPropertySymbols

Global Symbols

Although we know now how to get hidden properties using one of the methods above, it doesn’t tell us what the property is (other than via any optional labelling). Knowing the Symbol value for a given hidden property will get you there, but now you have to persist that symbol throughout your app either as a global value, or by passing it around as an argument, both of which could get messy.

Registering a global Symbol

There is a global store for symbols that you can access anywhere in your app, and you can add symbols to it using the Symbol.for() methods. Just like a local Symbol each one is unique, except this time, passing a name that’s already been created will return the same symbol as before

  const gs = Symbol.for('category')
console.log(gs.toString()) // Symbol(category)

// if we get that again - in this case it returns the same symbol
console.log(gs === Symbol.for("category")) // true
registering a global symbol with Symbol.for

Retrieving the name of a global symbol

If you have a global symbol, you can retrieve the name it represents

  console.log(Symbol.keyFor(gs)) // category
symbol.keyFor

Assigning global symbol property names

It’s exactly the same process as for local symbols

  data.forEach(d => d[Symbol.for('category')] = 'paint')
/**
* [ [ [ 'color', 'red' ],
[ 'id', 1 ],
[ 'Symbol(id)', 0 ],
[ 'Symbol(category)', 'paint' ] ],
[ [ 'color', 'blue' ],
[ 'id', 1 ],
[ 'Symbol(id)', 1 ],
[ 'Symbol(category)', 'paint' ] ] ]
*/
console.log(data.map(d => Reflect.ownKeys(d).map(k => [k.toString(), d[k]])));
assigning property names with global symbols

Checking for existence of property

As previously shown, Reflect can see symbol properties, so it  can be used to check on the existence of a hidden property

console.log(Reflect.has(data[0],Symbol.for('category') )); // true
Reflect.has

Global versus local symbols

This example shows global symbols are accessible from anywhere, whereas for local ones, you must already know the Symbol

  ((data) => {
// in this function we don't know the symbol for category, but want to access it
// because it's global- we can just do this
console.log(data.map(d => d[Symbol.for('category')])) // [ 'paint', 'paint' ]

// however, we don't know the symbol for id
// so this wont work as it'll just create another new symbol
console.log(data.map(d => d[Symbol('id')])) // [ undefined, undefined ]

// and neither will this, , as id wasn't a global symbol
// it'll just create another new global symbol for id
console.log(data.map(d => d[Symbol.for('id')])) // [ undefined, undefined ]

})(data)
accessing a global symbol from anywhere

Well known symbols

JavaScript has some built in Symbol properties. These are used to reference a range of methods for the object in which they exist. Think of .toString() and .valueOf() which exist in many objects are are used to provoke a certain formatting of an object. For example, console.log will try these methods to format an object for logging.

We all avoid property collisions by simply avoiding toString and valueOf properties or methods when creating an object, as they would override those in the object prototype. As more functionality arrives in JavaScript, it’s clearly not really a sustainable approach, and if JavaScript were to introduce new ones at this stage, they may well collide with methods already existing in your own objects.

That’s where well known (or built in) symbols come in. There are quite a few already in JavaScript, but in this article I’m just going to look at a couple.

Symbol.toPrimitive

This is a little like the .valueOf and .toString methods of an object combined.

console.log(object) – already has a default way of coercing an object so it can be displayed, but Symbol.toPrimitive is a new method that gives it a few more bells and whistles. Think of these 3 cases

  1. console.log(+object) -> convert the object to a number and print the result  // NaN
  2. console.log(`the color is ${object}`) -> convert the object to a string and print the combined string // the color is [object Object]
  3. console.log(object) -> just try to print out the objects contents // { color: ‘red’, id: 1 }
If we fiddle with Symbol.toPrimitive method, we can change that behavior.

  const fiddled = data.map(d => ({
...d,
[Symbol.toPrimitive](hint) {
if (hint === 'number') return this.id
else if (hint === 'string') return this.color
else return JSON.stringify(this)
}
}))

// this will log the ids
console.log(fiddled.map(f => f)) // [1, 1]

// this the colors
console.log(fiddled.map(f => `the color is ${f}`)) // [ 'the color is red', 'the color is blue' ]

// this stringifies each object
console.log(fiddled.map(f => f '')) // [ '{"color":"red","id":1}', '{"color":"blue","id":1}' ]
fiddling with Symbol.toPrimitive

The hint argument can be either string, number or default and will indicate the treatment to apply to the object.

Symbol.iterator

More interesting is the iterator Symbol. (there’s also a Symbol.asyncIterator which is a different method for dealing with async operations – for example of one of these see The nodejs Drive API, service account impersonation and async iterators )

When you do these things, it’s the method referred to by the built in Symbol.iterator that’s handling the iteration mechanics

  for (let d of data) {
console.log(d, d[Symbol.for('category')]) // // { color: 'red', id: 1 } 'paint' , { color: 'blue', id: 1 } 'paint'
}


console.log([...data]) // [ { color: 'red', id: 1 }, { color: 'blue', id: 1 } ]
the symbol.iterator

Checking for an iterator

We can check to see if something has a built in iterator using Reflect.has

console.log(Reflect.has(data, Symbol.iterator))  // true
console.log(Reflect.has(data[0], Symbol.iterator)) // false
Reflect.has iterator

Make your own iterator

You can change the iteration behavior simply by making your own. Here’s roughly what the built in Array iterator does

  // but you can make your own
// the built in iterator for an array looks like this
const makeIt = (arr) => {
let index = 0
return {
[Symbol.iterator]() {
return {
next() {
return index < arr.length ? {
value: arr[index ]
} : {
done: true
}
}
}
}
};
}
Array iterator

Using your own iterator

Now I can make my own iterator for my data array and it will use it instead. Of course this one just does the same as the built in one.

  const it = makeIt(data);

console.log([...it]) // [ { color: 'red', id: 1 }, { color: 'blue', id: 1 } ]
using your own iterator

Tweaking the iterator

Let’s make a fancier version of an Array iterator. In this case, we’ll add an option so that the values can be presented in reverse order if requried.

  const makeItFancy = (arr, reverse = false) => {
let index = 0
const getVal = () => reverse ? arr[arr.length - index] : arr[index ]
return {
[Symbol.iterator]() {
return {
next() {
return index < arr.length ? {
value: getVal()
} : {
done: true
}
}
}
}
}
}
tweaked iterator

Using the tweaked iterator

  console.log([...makeItFancy(data)])  // [ { color: 'red', id: 1 }, { color: 'blue', id: 1 } ]
console.log([...makeItFancy(data, true)]) // [ { color: 'blue', id: 1 }, { color: 'red', id: 1 } ]
result without and with reversing

Creating a brand new iterator

How about we make an iterator for an object (objects don’t have their own built in iterator), sort the keys alphabetically, and return an array of {key, value} for every property in the object

  const obit = (ob) => {
// just the visible properties
const keys = Object.keys(ob).sort()
let index = 0
const getVal = () => {
const key = keys[index ]
return {
key,
value: ob[key]
}
}
return {
[Symbol.iterator]() {
return {
next() {
return index < keys.length ? {
value: getVal()
} : {
done: true
}
}
}
}
}
}
Object key iterator

Using an object iterator

  console.log([...obit(data[0])]) // [ { key: 'color', value: 'red' }, { key: 'id', value: 1 } ]
an object iterator

Summary

There’s a lot more to say about Symbols, and in particular, how they have been used in Es6 to add new capabilities, but the intention was simply to try to demonstrate to you (and me) that there’s a lot going on with Symbols and they are a valuable addition to JavaScript (and Apps Script)