TypeScript Iterator Design Pattern

Ibrahim sengun
3 min readJan 20, 2024

--

What is iterator design pattern?

The iterator design pattern is a behavioral design pattern. It provides the ability to traverse through objects without delving into the objects’ inner structure or their underlying dependencies.

There are several terminologies in the iterator design pattern. These are:

  • Iterator: It defines the properties required for traversing.
  • Concrete iterators: It implement the iterator interface and handle specific algorithmic logic for traversing.
  • Collection: It defines the requirements for the iterator to work with collections.
  • Concrete collection: It implements the collection interface and, based on its definition, returns an iterator in each cycle.
  • Client: This refers to the application or function that communicates with the system.

When should the iterator design pattern be used?

The iterator design pattern can be used when there is a need to iterate over various types of collections while hiding the complexity beneath them. This way, instead of building a different type of iterator system for each type of collection, the iterator design pattern allows the complexity behind the collections to be ignored. It enables the construction of a uniform iterator system that can handle different types of collections effectively, avoiding their complexity and preventing code coupling.

How to implement iterator design pattern in TypeScript

Let’s apply the iterator design pattern to TypeScript. First, let’s imagine a scenario where we are trying to sort out our user base in an application, attempting to figure out which of our users are active and passive. However, the catch is that we have various types of user groups, making our job difficult. If we try to build an iterator system for each of these user groups, given their different structures and iterator systems, inevitably, it would increase our codebase complexity and decoupling.

So, we thought that if we could design a system where we could abstract these user groups and work on the surface with a uniform iterator system, we could solve all of our problems. That’s why we opted to use the iterator design pattern.

Iterator design pattern diagram

Iterator design pattern diagram

Iterator design pattern code

// User Template
class User {
public name: string;
public isActive: boolean;
constructor(name: string, isActive: boolean) {
this.name = name;
this.isActive = isActive;
}
}

// Iterator interface
interface Iterator<T> {
hasNext(): boolean;
next(): IteratorResult<T>;
}

// Collection interface
interface UserCollection {
createIterator(): Iterator<User>;
getUser(index: number): User;
length: number;
}

// Concrete Iterator
class UserIterator implements Iterator<User> {
private index: number = 0;
private collection: UserCollection;
constructor(collection: UserCollection) {
this.collection = collection;
}

hasNext(): boolean {
return this.index < this.collection.length;
}

next(): IteratorResult<User> {
if (this.hasNext()) {
const user = this.collection.getUser(this.index++);
return { value: user, done: false };
} else {
return { value: undefined as any, done: true };
}
}
}

// Concrete Collection
class UserManager implements UserCollection {
private users: User[] = [];

addUser(user: User): void {
this.users.push(user);
}

createIterator(): Iterator<User> {
return new UserIterator(this);
}

getUser(index: number): User {
return this.users[index];
}

get length(): number {
return this.users.length;
}
}

// Client
const userManager = new UserManager();

userManager.addUser(new User("ibrahim", true));
userManager.addUser(new User("sengun", false));

/*
ibrahim is active
sengun is passive
*/
const iterator = userManager.createIterator();
while (iterator.hasNext()) {
const userResult = iterator.next();
if (!userResult.done) {
const user = userResult.value;
const status = user.isActive ? "active" : "passive";
console.log(`${user.name} is ${status}`);
}
}

--

--