Understanding Iterator Pattern in JavaScript/Typescript using Symbol.Iterator
In the Design Patterns series that I am writing to illustrate the 23 design patterns of the band of four (GoF) in a friendly way I recently wrote about the Iterator pattern. This pattern has an incredible potential to decouple our data structure from the algorithms.
This pattern is widely implemented in different programming languages, for example, JAVA has the Iterable Interface.
In some languages there are high-level control structures that allow you to iterate the objects without having to create the Iterator pattern (since they provide it by default). However, it may be useful to modify this internal pattern to use the full potential of the language without the need for great verbosity, as in JAVA.
It is essential to know about the Iterator pattern and to have read the article that I wrote, since the examples that will illustrate this article come from the previous one.
Iterators in JavaScript/TypeScript
ES6 introduces a new way to interact with JavaScript data structures — iteration. There are 2 core concepts for Iterator Pattern:
- Iterable is a data structure that provides a way to expose its data to the public. In JavaScript the implementation is based on a method whose key is Symbol.iterator. Really, Symbol.iterator is a factory of iterators.
- Iterator is a structure that contains a pointer to the next element in the iteration.
Therefore, an iterable must be an object with a function iterator whose key is Symbol.iterator
.
const iterable {
[Symbol.iterator](){
// Any code
}
}
/*********/
class iterable {
[Symbol.iterator](){
// Any code
}
}
Furthermore, an iterator must be an object with a function named next that returns an object with the keys:
value
: the current item in the iterationdone
:true
if the iteration has finished,false
otherwise.
const iterable = {
[Symbol.iterator](){
let step = 0;
const iterator = {
next() {
step++;
if(step < 3){
return { value: step; done: false };
}
return { value: null; done: true };
}
};
return iterator;
}
}
/* ******* */
class iterable {
[Symbol.iterator](){
let step = 0;
const iterator = {
next() {
step++;
if(step < 3){
return { value: step; done: false };
}
return { value: null; done: true };
}
};
return iterator;
}
}
So, the previous code defines an iterable and an iterator which are used in the following way:
const iteratble = new Iterable();
iterable.next(); // { value: 1; done: false }
iterable.next(); // { value: 2; done: false }
iterable.next(); // { value: null; done: true }
When you use a for-of
loop the language is using an iterable, and creating its iterator. It keeps on calling the next()
until done
is true.
There are a list of iterables in JavaScript which are using the concepts previously mentioned natively.
Arrays
andTypedArrays
over each element inside array.Strings
over each character.Sets
over their elements.Maps
over its key-value pairs.
Some other constructs in JavaScript that use iterables are the following ones:
for-of loop
which uses an iterable, otherwise it will throw an Error.
for (const value of iterable) {....}
- Destructuring of Array and spread operator
const numbers = [1, 2, 3, 4, 5];
const [first, ,third, ,last] = numbers;
// is equivalent to
const numbers = [1, 2, 3, 4, 5];
const iterator = numbers[Symbol.iterator]();
const first = iterator.next().value
iterator.next().value
const third = iterator.next().value
iterator.next().value
const last = iterator.next().value
Iterator pattern - Example 1: Words Collection
I will now show you how you can implement this pattern using JavaScript/TypeScript Symbols. In our case, I have made up a problem in which there is a class named WordsCollection
which defines a word's list (items
) and its set of methods to get and add (getItems
and addItem
). This class is used by the client
using control structures, such as for
or forEach
. The following UML diagram shows the scenario that I have just described using the classical iterator pattern.
Now I'm going to show you the new UML diagram which maintains the compatibility with custom iterators and implements the native ones.
First, let's start with the client that will use the iterator natively without the need to instantiate any classes. It will perform the iteration directly from the for-of loop.
import { WordsCollection } from './words-collection';
const collection = new WordsCollection();
collection.addItem('First');
collection.addItem('Second');
collection.addItem('Third');
console.log('Native Iterator');
for (const c of collection) {
console.log(c);
}
console.log('');
console.log('Reverse Iterator');
for (const c of collection.reverse()) {
console.log(c);
}
console.log('Custom Iterator');
const iterator = collection.getIterator();
console.log('Straight traversal:');
while (iterator.hasMoreElements()) {
console.log(iterator.next());
}
console.log('');
console.log('Reverse traversal:');
const reverseIterator = collection.getReverseIterator();
while (reverseIterator.hasMoreElements()) {
console.log(reverseIterator.next());
}
Note that the native use of iterators on a language cleaner, since it is integrated with the control structures of the programming language.
The WordsCollection
code associated is the following one:
import {
CustomAlphabeticalOrderIterator,
NativeAlphabeticalOrderIterator
} from './alphabetical-order-iterator';
import { Aggregator } from './agregator.interface';
import { CustomIterator } from './iterator.interface';
export class WordsCollection implements Aggregator, Iterable<any> {
private items: string[] = [];
public getItems(): string[] {
return this.items;
}
public getCount(): number {
return this.items.length;
}
public addItem(item: string): void {
this.items.push(item);
}
[Symbol.iterator](): Iterator<string> {
return new NativeAlphabeticalOrderIterator(this);
}
reverse(): Iterable<string> {
return {
[Symbol.iterator]: () => {
return new NativeAlphabeticalOrderIterator(this, true);
}
};
}
public getIterator(): CustomIterator<string> {
return new CustomAlphabeticalOrderIterator(this);
}
public getReverseIterator(): CustomIterator<string> {
return new CustomAlphabeticalOrderIterator(this, true);
}
}
The first thing we have to observe is that we need to implement two interfaces:
- Aggregator is used for custom Iterable and defines the necessary methods for the object to be iterable. Here we have to implement the constructor methods of the iterators.
- Iterator is used by the language natively, and is the one that defines the function [Symbol.iterator]: ().
The keys to the native iterator are the factory functions of the native iterator, which instead of directly implementing it has been extracted in a new class to keep the code as clean as in the previous solution (NativeAlphabeticalOrderIterator
).
[Symbol.iterator](): Iterator<string> {
return new NativeAlphabeticalOrderIterator(this);
}
reverse(): Iterable<string> {
return {
[Symbol.iterator]: () => {
return new NativeAlphabeticalOrderIterator(this, true);
}
};
}
Finally, both the native iterator and the custom iterator extend their next()
method from an abstract iterator which implements the rest of the methods. The main difference between the next()
methods from the iterators are the values returned by each one of them, since in the native iterator the Iterator
interface must be satisfied, which returns an IteratorResult
while the CustomIterator
returns the value directly.
This code could even be refactored to be used together with other design patterns such as Template-Method or Strategy. However, I think it is better not to further complicate the example with the addition of these patterns.
import { CustomIterator } from './iterator.interface';
import { WordsCollection } from './words-collection';
export abstract class AlphabeticalOrderIterator {
private collection: WordsCollection;
protected position: number = 0;
protected reverse: boolean = false;
constructor(collection: WordsCollection, reverse: boolean = false) {
this.collection = collection;
this.reverse = reverse;
if (reverse) {
this.position = collection.getCount() - 1;
}
}
public current(): string {
return this.collection.getItems()[this.position];
}
public hasMoreElements(): boolean {
if (this.reverse) {
return this.position >= 0;
}
return this.position < this.collection.getCount();
}
}
export class NativeAlphabeticalOrderIterator extends AlphabeticalOrderIterator
implements Iterator<string> {
public next() {
const item = this.current();
const result = { value: item, done: !this.hasMoreElements() };
this.position += this.reverse ? -1 : 1;
return result;
}
}
export class CustomAlphabeticalOrderIterator extends AlphabeticalOrderIterator
implements CustomIterator<string> {
public next() {
const item = this.current();
this.position += this.reverse ? -1 : 1;
return item;
}
}
The client is decoupled from the internal structure of the WordsCollection class
(Single Responsibility) and you can extend the software implementing new iterators (Open/Closed).
I have created two npm scripts that run the code example shown here after applying the Iterator pattern.
npm run example1-problem
npm run example1-iterator-solution-1
Iterator pattern - Example 2: Dev.To and Medium Social Networks!
The following example is described extensively in the following article, therefore I recommend that you read this article to understand this. However, just to give you an idea of what we are developing I will give you a brief description of the problem.
Imagine that we have to create a software that allows us to send emails to our contacts in social networks, taking into account that we are going to differentiate the type of mail to send. In our network of contacts we have two categories of contacts: Friends and Coworkers. The email to be sent will be more formal depending on the type of contact to which the email will be sent.
At first we have contacts from two famous social networks: Dev.to and Medium. The implementation of the data structure of each of the social networks is different, since in Dev.to an array
is used to maintain the contacts while in Medium a Map
is used.
You can find a gif below showing the client using our entire structure (I have done a small CLI example).
In the following UML diagram you can see the solution proposed for this problem using CustomIterator:
The diagram associated with the solution that includes native iterators is as follows. In any case, we continue to maintain compatibility with custom iterators.
The main advantages of developing decoupled software is that, as our software grows, it is not affected by the changes. In fact, the client of our application is still the same piece of code since it is built based on interfaces and using dependency injection.
const { network: networkSelected } = answers;
const mockProfiles = createTestProfiles();
const network: SocialNetwork =
networkSelected === 'Dev.to'
? new DevTo(mockProfiles)
: new Medium(mockProfiles);
const spammer = new SocialSpammer(network);
spammer.sendSpamToFriends(
'ben@dev.to',
"Hey! This is Ben's friend Jessi. Can you do me a favor and like this post [link]?"
);
spammer.sendSpamToCoworkers(
'ben@dev.to',
"Hey! This is Ben's boss Jess. Ben told me you would be interested in [link]."
);
The sendSpamToFriends
and sendSpamToCoworkers
methods use the iterators, either custom or native.
import { SocialNetwork } from './social-network.interface';
export class SocialSpammer {
public network: SocialNetwork;
public constructor(network: SocialNetwork) {
this.network = network;
}
public sendSpamToFriends(profileEmail: string, message: string): void {
console.log('\nIterating over friends (custom)...\n');
const iterator = this.network.createFriendsIterator(profileEmail);
while (iterator.hasNext()) {
const profile = iterator.next();
this.sendMessage(profile.email, message);
}
console.log('\nIterating over friends (native)...\n');
for (const profile of this.network.createNativeIterator(
'friends',
profileEmail
)) {
this.sendMessage(profile.email, message);
}
}
public sendSpamToCoworkers(profileEmail: string, message: string): void {
console.log('\nIterating over coworkers (custom)...\n');
const iterator = this.network.createCoworkersIterator(profileEmail);
while (iterator.hasNext()) {
const profile = iterator.next();
this.sendMessage(profile.email, message);
}
console.log('\nIterating over coworkes (native)...\n');
for (const profile of this.network.createNativeIterator(
'coworkers',
profileEmail
)) {
this.sendMessage(profile.email, message);
}
}
private sendMessage(email: string, message: string): void {
console.log(
"Sent message to: '" + email + "'. Message body: '" + message + "'"
);
}
}
In this method we use custom and native iterators. The creation of the native iterators has been delegated to an iterators' factory to discern between friends or coworkers.
The social networks (dev.to or medium) must satisfy the SocialNetwork
interface by creating the native iterator through the object of the Iterable <Profile>
type. The code associated to the social networks is the following one:
import { CustomDevToIterator, NativeDevToIterator } from './dev-to-iterator';
import { Profile } from '../profile.class';
import { ProfileIterator } from '../profile-iterator.interface';
import { SocialNetwork } from '../social-network.interface';
export class DevTo implements SocialNetwork {
private contacts: Array<Profile>;
constructor(cache: Array<Profile>) {
this.contacts = cache || [];
}
public requestContactInfoFromDevToInAPI(profileEmail: string): Profile {
// Here would be a POST request to one of the DevTo API endpoints.
console.log(`DevTo: Loading profile ${profileEmail} over the network...`);
// ...and return test data.
return this.findContact(profileEmail);
}
public requestRelatedContactsFromDevToInAPI(
profileEmail: string,
contactType: string
): Array<string> {
// Here would be a POST request to one of the DevTo API endpoints.
console.log(
`DevTo: Loading ${contactType} list of ${profileEmail} over the network...`
);
// ...and return test data.
const profile = this.findContact(profileEmail);
return profile ? profile.getContactsByType(contactType) : null;
}
private findContact(profileEmail: string): Profile {
return this.contacts.find(profile => profile.email === profileEmail);
}
public createNativeIterator(
type: string,
profileEmail: string
): Iterable<Profile> {
return {
[Symbol.iterator]: () => {
return new NativeDevToIterator(this, type, profileEmail);
}
};
}
public createFriendsIterator(profileEmail: string): ProfileIterator {
return new CustomDevToIterator(this, 'friends', profileEmail);
}
public createCoworkersIterator(profileEmail: string): ProfileIterator {
return new CustomDevToIterator(this, 'coworkers', profileEmail);
}
}
import { CustomMediumIterator, NativeMediumIterator } from './medium-iterator';
import { Profile } from '../profile.class';
import { ProfileIterator } from '../profile-iterator.interface';
import { SocialNetwork } from '../social-network.interface';
export class Medium implements SocialNetwork {
private contacts: Map<string, Profile>;
constructor(cache: Array<Profile>) {
this.contacts = new Map();
if (cache) {
for (const profile of cache) {
this.contacts.set(profile.email, profile);
}
}
}
public requestContactInfoFromMediumInAPI(profileEmail: string): Profile {
// Here would be a POST request to one of the Medium API endpoints.
console.log(`Medium: Loading profile ${profileEmail} over the network...`);
// ...and return test data.
return this.findContact(profileEmail);
}
public requestRelatedContactsFromMediumInAPI(
profileEmail: string,
contactType: string
): Array<string> {
// Here would be a POST request to one of the DevTo API endpoints.
console.log(
`Medium: Loading ${contactType} list of ${profileEmail} over the network...`
);
// ...and return test data.
const profile = this.findContact(profileEmail);
return profile ? profile.getContactsByType(contactType) : null;
}
private findContact(profileEmail: string): Profile {
return this.contacts.get(profileEmail);
}
public createNativeIterator(type: string, profileEmail: string) {
return {
[Symbol.iterator]: () => {
return new NativeMediumIterator(this, type, profileEmail);
}
};
}
public createFriendsIterator(profileEmail: string): ProfileIterator {
return new CustomMediumIterator(this, 'friends', profileEmail);
}
public createCoworkersIterator(profileEmail: string): ProfileIterator {
return new CustomMediumIterator(this, 'coworkers', profileEmail);
}
}
Each of the social networks (dev.to or medium) must satisfy the SocialNetwork
interface by creating the native iterator through the object of the Iterable type <Profile>
.
Below is the code associated with the iterators. Both iterators (custom and native) extends from a parent iterator where the methods in common have been implemented. Although the Iterator
interface has several methods in our example, we only need to implement the next()
method.
import { DevTo } from './dev-to.class';
import { Profile } from '../profile.class';
import { ProfileIterator } from '../profile-iterator.interface';
export abstract class DevToIterator {
protected devTo: DevTo;
protected type: string;
protected email: string;
protected currentPosition = 0;
protected emails: string[] = [];
protected contacts: Profile[] = [];
public constructor(devTo: DevTo, type: string, email: string) {
this.devTo = devTo;
this.type = type;
this.email = email;
}
private lazyLoad(): void {
if (this.emails.length === 0) {
const profiles = this.devTo.requestRelatedContactsFromDevToInAPI(
this.email,
this.type
);
for (const profile of profiles) {
this.emails.push(profile);
this.contacts.push(null);
}
}
}
public hasNext(): boolean {
this.lazyLoad();
return this.currentPosition < this.emails.length;
}
public reset(): void {
this.currentPosition = 0;
}
}
export class NativeDevToIterator extends DevToIterator
implements Iterator<Profile> {
public next(): IteratorResult<Profile> {
if (!this.hasNext()) {
return { value: null, done: true };
}
const friendEmail = this.emails[this.currentPosition];
let friendContact = this.contacts[this.currentPosition];
if (!friendContact) {
friendContact = this.devTo.requestContactInfoFromDevToInAPI(friendEmail);
this.contacts.splice(this.currentPosition, 1, friendContact);
}
const result = { value: friendContact, done: !this.hasNext() };
this.currentPosition++;
return result;
}
}
export class CustomDevToIterator extends DevToIterator
implements ProfileIterator {
public next(): Profile {
if (!this.hasNext()) {
return null;
}
const friendEmail = this.emails[this.currentPosition];
let friendContact = this.contacts[this.currentPosition];
if (!friendContact) {
friendContact = this.devTo.requestContactInfoFromDevToInAPI(friendEmail);
this.contacts.splice(this.currentPosition, 1, friendContact);
}
this.currentPosition++;
return friendContact;
}
}
The iterators associated to medium correspond to the same interface as those of dev.to and are shown below:
import { Medium } from './medium.class';
import { Profile } from '../profile.class';
import { ProfileIterator } from '../profile-iterator.interface';
export abstract class MediumIterator {
protected medium: Medium;
protected type: string;
protected email: string;
protected currentPosition = 0;
protected emails: string[] = [];
protected contacts: Profile[] = [];
public constructor(medium: Medium, type: string, email: string) {
this.medium = medium;
this.type = type;
this.email = email;
}
private lazyLoad(): void {
if (this.emails.length === 0) {
const profiles = this.medium.requestRelatedContactsFromMediumInAPI(
this.email,
this.type
);
for (const profile of profiles) {
this.emails.push(profile);
this.contacts.push(null);
}
}
}
public hasNext(): boolean {
this.lazyLoad();
return this.currentPosition < this.emails.length;
}
public reset(): void {
this.currentPosition = 0;
}
}
export class NativeMediumIterator extends MediumIterator
implements Iterator<Profile> {
public next() {
if (!this.hasNext()) {
return null;
}
const friendEmail = this.emails[this.currentPosition];
let friendContact = this.contacts[this.currentPosition];
if (!friendContact) {
friendContact = this.medium.requestContactInfoFromMediumInAPI(
friendEmail
);
this.contacts.splice(this.currentPosition, 1, friendContact);
}
this.currentPosition++;
const result = { value: friendContact, done: this.hasNext() };
return result;
}
}
export class CustomMediumIterator extends MediumIterator
implements ProfileIterator {
public next() {
if (!this.hasNext()) {
return null;
}
const friendEmail = this.emails[this.currentPosition];
let friendContact = this.contacts[this.currentPosition];
if (!friendContact) {
friendContact = this.medium.requestContactInfoFromMediumInAPI(
friendEmail
);
this.contacts.splice(this.currentPosition, 1, friendContact);
}
this.currentPosition++;
return friendContact;
}
}
I have created an npm script that runs the example shown here after applying the Iterator pattern and a CLI interface.
npm run example2-iterator-solution1
Conclusion
Iterator pattern can avoid coupled code in your projects. When there are several algorithms and data structures in a collection the iterator pattern is perfectly adapted. Your code will be cleaner, since you apply two famous principles, such as Single Responsibility and Open/Closed.
Sometimes we are interested in knowing the programming language on which we are developing and check if the Iterator pattern is inherently implemented as it happens in languages such as JavaScript (shown throughout this post), Python or PHP.
The most important thing is not to implement the pattern as I have shown you, but to be able to recognise the problem which this specific pattern can resolve, and when you may or may not implement said pattern. This is crucial, since implementation will vary depending on the programming language you use.