Skip to main content

TypeScript Iterators

Introduction

Iterators are one of TypeScript's powerful features that allow you to traverse through collections of data. They provide a standard way to access elements in a sequence without exposing the underlying implementation details. If you've ever used a for...of loop in TypeScript or JavaScript, you've already been using iterators without knowing it!

In this tutorial, we'll explore TypeScript iterators in depth, understanding how they work, how to implement them, and how they can make your code more elegant when dealing with collections.

What are Iterators?

In TypeScript, an iterator is an object that implements the Iterator interface, which provides a way to access elements in a collection sequentially. The core of this interface is the next() method, which returns an object with two properties:

  • value: The current element in the sequence
  • done: A boolean indicating whether the sequence has been fully traversed

Let's look at the TypeScript interface for iterators:

typescript
interface Iterator<T> {
next(): { value: T; done: boolean };
return?(value?: any): { value: any; done: boolean };
throw?(e?: any): { value: any; done: boolean };
}

What Makes Something Iterable?

For an object to be iterable, it needs to implement the Iterable interface, which requires a method called Symbol.iterator that returns an iterator:

typescript
interface Iterable<T> {
[Symbol.iterator](): Iterator<T>;
}

Creating a Simple Iterator

Let's create a simple iterator that counts from 1 to a specified number:

typescript
class Counter implements Iterable<number> {
constructor(private limit: number) {}

[Symbol.iterator](): Iterator<number> {
let counter = 0;
const limit = this.limit;

return {
next(): { value: number; done: boolean } {
counter++;

if (counter <= limit) {
return { value: counter, done: false };
} else {
return { value: undefined!, done: true };
}
}
};
}
}

// Using our Counter
const counter = new Counter(5);

// With for...of loop
for (const count of counter) {
console.log(count); // Output: 1, 2, 3, 4, 5
}

// Manually using the iterator
const iterator = counter[Symbol.iterator]();
console.log(iterator.next()); // Output: { value: 1, done: false }
console.log(iterator.next()); // Output: { value: 2, done: false }
console.log(iterator.next()); // Output: { value: 3, done: false }
console.log(iterator.next()); // Output: { value: 4, done: false }
console.log(iterator.next()); // Output: { value: 5, done: false }
console.log(iterator.next()); // Output: { value: undefined, done: true }

Built-in Iterables

In TypeScript (and JavaScript), several built-in types are already iterable:

  • Arrays
  • Strings
  • Maps
  • Sets
  • DOM NodeLists
  • and more!

This means you can use a for...of loop with them:

typescript
// Iterating over an array
const numbers = [1, 2, 3, 4, 5];
for (const num of numbers) {
console.log(num);
}

// Iterating over a string
const greeting = "Hello";
for (const char of greeting) {
console.log(char);
}

// Iterating over a Map
const userRoles = new Map<string, string>([
["John", "Admin"],
["Sarah", "Editor"],
["Mike", "User"]
]);

for (const [user, role] of userRoles) {
console.log(`${user}: ${role}`);
}

Creating a Custom Iterable Collection

Let's implement a custom collection that's iterable. We'll create a simple Queue class:

typescript
class Queue<T> implements Iterable<T> {
private items: T[] = [];

enqueue(item: T): void {
this.items.push(item);
}

dequeue(): T | undefined {
return this.items.shift();
}

peek(): T | undefined {
return this.items[0];
}

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

[Symbol.iterator](): Iterator<T> {
let index = 0;
const items = this.items;

return {
next(): { value: T; done: boolean } {
if (index < items.length) {
return { value: items[index++], done: false };
} else {
return { value: undefined!, done: true };
}
}
};
}
}

// Using our Queue
const taskQueue = new Queue<string>();
taskQueue.enqueue("Write tutorial");
taskQueue.enqueue("Create examples");
taskQueue.enqueue("Edit content");

// Iterate through the queue without modifying it
for (const task of taskQueue) {
console.log(`Pending task: ${task}`);
}

// Output:
// Pending task: Write tutorial
// Pending task: Create examples
// Pending task: Edit content

// The queue remains intact
console.log(`Next task: ${taskQueue.peek()}`); // Output: Next task: Write tutorial

The IterableIterator Interface

TypeScript provides an IterableIterator interface that combines both Iterable and Iterator. This is useful when you want an object to both be iterable and have iterator methods directly available:

typescript
interface IterableIterator<T> extends Iterator<T> {
[Symbol.iterator](): IterableIterator<T>;
}

Here's an example implementation:

typescript
class RangeIterator implements IterableIterator<number> {
private current: number;

constructor(private start: number, private end: number) {
this.current = start;
}

public next(): { value: number; done: boolean } {
if (this.current <= this.end) {
return { value: this.current++, done: false };
} else {
return { value: undefined!, done: true };
}
}

[Symbol.iterator](): IterableIterator<number> {
return this;
}
}

// Using RangeIterator
const range = new RangeIterator(1, 5);

// Direct iterator methods
console.log(range.next().value); // Output: 1
console.log(range.next().value); // Output: 2

// Using it in a for-of loop (starts from current value)
for (const num of range) {
console.log(num); // Output: 3, 4, 5
}

Using Generators for Easy Iterator Creation

Creating iterators can involve a lot of boilerplate code. TypeScript supports generator functions (inherited from JavaScript) that make creating iterators much simpler. A generator function uses the function* syntax and yield keyword:

typescript
function* countUp(limit: number): Generator<number> {
for (let i = 1; i <= limit; i++) {
yield i;
}
}

// Using the generator
const counter = countUp(3);
console.log(counter.next()); // Output: { value: 1, done: false }
console.log(counter.next()); // Output: { value: 2, done: false }
console.log(counter.next()); // Output: { value: 3, done: false }
console.log(counter.next()); // Output: { value: undefined, done: true }

// Using in a for-of loop
for (const value of countUp(3)) {
console.log(value); // Output: 1, 2, 3
}

You can also create class-based iterables more elegantly with generators:

typescript
class FibonacciSequence implements Iterable<number> {
constructor(private limit: number) {}

*[Symbol.iterator](): Generator<number> {
let prev = 0;
let curr = 1;

yield prev; // Yield the first value (0)

if (this.limit >= 1) {
yield curr; // Yield the second value (1)
}

// Generate the rest of the sequence
for (let i = 2; i < this.limit; i++) {
const next = prev + curr;
yield next;
prev = curr;
curr = next;
}
}
}

// Using our Fibonacci sequence
const fib = new FibonacciSequence(8);
for (const num of fib) {
console.log(num);
}
// Output: 0, 1, 1, 2, 3, 5, 8, 13

Real-World Applications

Iterators are particularly useful in the following scenarios:

1. Lazy Evaluation

Iterators can generate values on-demand, which is memory efficient for large or infinite sequences:

typescript
function* infinitePrimes(): Generator<number> {
function isPrime(n: number): boolean {
if (n <= 1) return false;
if (n <= 3) return true;
if (n % 2 === 0 || n % 3 === 0) return false;

let i = 5;
while (i * i <= n) {
if (n % i === 0 || n % (i + 2) === 0) return false;
i += 6;
}
return true;
}

let num = 2;
while (true) {
if (isPrime(num)) {
yield num;
}
num++;
}
}

// Get the first 5 prime numbers
const primeGen = infinitePrimes();
for (let i = 0; i < 5; i++) {
console.log(primeGen.next().value);
}
// Output: 2, 3, 5, 7, 11

2. Custom Data Structure Traversal

Iterators provide a consistent interface for traversing different data structures:

typescript
class BinaryTreeNode<T> {
constructor(
public value: T,
public left?: BinaryTreeNode<T>,
public right?: BinaryTreeNode<T>
) {}
}

class BinaryTree<T> implements Iterable<T> {
constructor(public root?: BinaryTreeNode<T>) {}

// In-order traversal iterator
*[Symbol.iterator](): Generator<T> {
// Helper generator function for recursion
function* traverse(node?: BinaryTreeNode<T>): Generator<T> {
if (node) {
// Traverse left subtree
yield* traverse(node.left);
// Visit current node
yield node.value;
// Traverse right subtree
yield* traverse(node.right);
}
}

yield* traverse(this.root);
}
}

// Create a sample tree:
// 4
// / \
// 2 6
// / \ / \
// 1 3 5 7

const tree = new BinaryTree<number>(
new BinaryTreeNode(4,
new BinaryTreeNode(2,
new BinaryTreeNode(1),
new BinaryTreeNode(3)
),
new BinaryTreeNode(6,
new BinaryTreeNode(5),
new BinaryTreeNode(7)
)
)
);

// Traverse the tree in-order
for (const value of tree) {
console.log(value);
}
// Output: 1, 2, 3, 4, 5, 6, 7 (sorted order)

3. Processing Streams of Data

Iterators are great for working with potentially unlimited data streams:

typescript
class DataStream<T> implements Iterable<T> {
private buffer: T[] = [];
private closed = false;

// Add data to the stream
addData(data: T): void {
if (!this.closed) {
this.buffer.push(data);
}
}

// Close the stream
close(): void {
this.closed = true;
}

// Create an async iterator for this stream
async *[Symbol.asyncIterator](): AsyncGenerator<T> {
let index = 0;

while (true) {
// If we have data available, yield it
if (index < this.buffer.length) {
yield this.buffer[index++];
continue;
}

// If the stream is closed and we've read all data, we're done
if (this.closed) {
break;
}

// Otherwise, wait for new data
await new Promise(resolve => setTimeout(resolve, 100));
}
}
}

// Example usage (this would normally be in an async function)
async function processStream() {
const dataStream = new DataStream<number>();

// Simulate data coming in over time
setTimeout(() => dataStream.addData(1), 100);
setTimeout(() => dataStream.addData(2), 300);
setTimeout(() => dataStream.addData(3), 600);
setTimeout(() => dataStream.close(), 1000);

// Process the stream as data becomes available
for await (const data of dataStream) {
console.log(`Processed: ${data}`);
}

console.log("Stream processing completed");
}

// processStream();
// Output (with delays):
// Processed: 1
// Processed: 2
// Processed: 3
// Stream processing completed

Summary

TypeScript iterators provide a powerful abstraction for working with sequences of data. They allow you to:

  • Create custom collection types with standardized iteration behavior
  • Process data lazily, improving memory efficiency
  • Implement complex traversal patterns for data structures
  • Work with potentially unlimited data streams
  • Write more concise and readable code when dealing with collections

By mastering iterators, you gain access to a more functional and declarative programming style that can handle complex data processing tasks elegantly.

Exercises

  1. Create an iterator for a Stack<T> class that iterates from top to bottom.
  2. Implement a Range class that uses a generator to produce a sequence of numbers between a start and end value with a customizable step.
  3. Create a CircularBuffer<T> class with a fixed size that implements both Iterable<T> and contains a push(item: T) method that overwrites the oldest items when the buffer is full.
  4. Implement a zip function that takes two iterables and produces a new iterable containing pairs of elements from the input iterables.
  5. Create a filterIterator function that takes an iterable and a predicate function, returning a new iterable containing only elements that satisfy the predicate.

Additional Resources

Happy iterating!



If you spot any mistakes on this website, please let me know at [email protected]. I’d greatly appreciate your feedback! :)