Making a Data Structure Iterable in JavaScript & TypeScript
In my last post I explained how to make a Map and Set that allowed you to use coordinates ([number, number] arrays) more like values than references. This means that adding a coordinate array with the same values twice will only result in one entry in the respective data structures. For example, in the CoordinateSet
data structure you can see this behaviour where we add the same coordinate twice but the size remains at 0:
coordinateSet.add([51.509865, -0.118092]);
coordinateSet.add([51.509865, -0.118092]);
coordinateSet.size; // 1
Here I realised I wanted to extend CoordinateSet
to be closer to the Set
class. This meant adding the entries
and forEach
methods and also handling spreadable and for...of
behaviour.
This got me looking at how these work for Set. Turns out set implements Set.prototype[@@iterator]()
, which allows it to be iterated over. An object is considered iterable if the object implements the iterable protocol - some examples include Arrays, Maps and Sets.
The @@iterator
property is accessible via Symbol.iterator
, and must return an object that conforms to the iterator protocol. Luckily, using a generator function we can ensure that an iterator object is returned. Generator functions are pretty cool and can be defined by adding an asterix onto the end of a function keyword, i.e. function*
. In this specific case we use a computed property, which means we are defining it using a value, in this case Symbol.iterator
. This syntax is slightly different as we have to put the asterix before *[Symbol.iterator]()
.
Let's assume we have a Class (in our case CoordinateSet
from the previous post), here's how we'd do it:
// This bit allows the data structure to be iterated over with
// spread syntax ([...]) and for...of
*[Symbol.iterator]() {
for (let entry of this.map.entries()) {
yield [entry[1], entry[1]];
}
}
// Matching the Set.entries interface
entries() {
return this[Symbol.iterator]();
}
Let's break down the bits that might be a bit alien here:
*
denotes a generator - this means that the function is a generator function, where the function returns a Generator object. The Generator object implements both the iterable protocol and the iterator protocol.[Symbol.iterator]
- this is the computed property denoted by the square brackets. TheSymbol.iterator
is a Symbol which specifies the default iterator for a given object. It's outside the scope of the post to go into Symbols but you can think of them as key that's guaranteed to be unique. There are a few Symbols that are builtin in, such asSymbol.iterator
.yield
is similar to return, but instead pauses the generator function execution and the value that is yielded is returned to the generator
There's quite a bit to take in there if you're not familiar with generators and Symbols. I would recommend taking a bit of a look at the MDN documentation for these two and the above code will start to make a little more sense!
Published