Prerequisites
- Basic understanding of JavaScript ๐
- TypeScript installation โก
- VS Code or preferred IDE ๐ป
What you'll learn
- Understand the concept fundamentals ๐ฏ
- Apply the concept in real projects ๐๏ธ
- Debug common issues ๐
- Write type-safe code โจ
๐ฏ Introduction
Welcome to this exciting tutorial on the Iterator Pattern! ๐ In this guide, weโll explore how to traverse collections in a clean, type-safe way using TypeScript.
Youโll discover how the Iterator Pattern can transform your approach to handling collections. Whether youโre building playlists ๐ต, managing inventories ๐ฆ, or processing data streams ๐, understanding iterators is essential for writing elegant, maintainable code.
By the end of this tutorial, youโll feel confident implementing custom iterators in your own projects! Letโs dive in! ๐โโ๏ธ
๐ Understanding Iterator Pattern
๐ค What is the Iterator Pattern?
The Iterator Pattern is like a tour guide ๐บ๏ธ for your data collections. Think of it as a bookmark ๐ that remembers where you are in a collection and helps you move through it step by step.
In TypeScript terms, an iterator provides a standardized way to traverse collections without exposing their internal structure. This means you can:
- โจ Access elements sequentially
- ๐ Support different traversal algorithms
- ๐ก๏ธ Keep collection internals private
๐ก Why Use Iterator Pattern?
Hereโs why developers love iterators:
- Uniform Interface ๐: Same API for different collections
- Separation of Concerns ๐ป: Collection logic stays separate from traversal
- Multiple Iterations ๐: Support multiple simultaneous iterations
- Lazy Evaluation ๐ง: Process elements on-demand
Real-world example: Imagine browsing a music playlist ๐ต. The iterator lets you skip to the next song without knowing how the playlist stores its tracks!
๐ง Basic Syntax and Usage
๐ Simple Iterator Implementation
Letโs start with a friendly example:
// ๐ Hello, Iterator Pattern!
interface Iterator<T> {
hasNext(): boolean; // ๐ Check if more elements exist
next(): T; // โก๏ธ Get next element
current(): T; // ๐ Get current element
}
// ๐จ Creating a simple collection
class NumberCollection {
private items: number[] = [];
constructor(items: number[]) {
this.items = items;
}
// ๐ Create an iterator
createIterator(): NumberIterator {
return new NumberIterator(this.items);
}
}
// ๐ฏ The iterator implementation
class NumberIterator implements Iterator<number> {
private items: number[];
private index: number = 0;
constructor(items: number[]) {
this.items = items;
}
hasNext(): boolean {
return this.index < this.items.length;
}
next(): number {
if (!this.hasNext()) {
throw new Error("๐ซ No more elements!");
}
return this.items[this.index++];
}
current(): number {
return this.items[this.index];
}
}
๐ก Explanation: The iterator keeps track of position without exposing the array directly. Clean separation! ๐ฏ
๐ฏ Using Built-in Iterators
TypeScript supports the ES6 iteration protocol:
// ๐๏ธ Using Symbol.iterator
class PlayList {
private songs: string[] = [];
addSong(song: string): void {
this.songs.push(song);
console.log(`๐ต Added "${song}" to playlist!`);
}
// ๐ Make it iterable
[Symbol.iterator](): Iterator<string> {
let index = 0;
const songs = this.songs;
return {
next(): IteratorResult<string> {
if (index < songs.length) {
return { value: songs[index++], done: false };
}
return { value: undefined as any, done: true };
}
};
}
}
// ๐ฎ Use with for...of
const myPlaylist = new PlayList();
myPlaylist.addSong("TypeScript Rocks");
myPlaylist.addSong("Iterator Groove");
for (const song of myPlaylist) {
console.log(`๐ง Now playing: ${song}`);
}
๐ก Practical Examples
๐ Example 1: Shopping Cart Iterator
Letโs build something real:
// ๐๏ธ Define our product type
interface Product {
id: string;
name: string;
price: number;
emoji: string;
}
// ๐ Shopping cart with custom iterator
class ShoppingCart {
private items: Product[] = [];
// โ Add product
addProduct(product: Product): void {
this.items.push(product);
console.log(`${product.emoji} Added ${product.name} to cart!`);
}
// ๐ฐ Get total using iterator
getTotal(): number {
let total = 0;
const iterator = this.createIterator();
while (iterator.hasNext()) {
const product = iterator.next();
total += product.price;
}
return total;
}
// ๐ฏ Create custom iterator
createIterator(): CartIterator {
return new CartIterator(this.items);
}
// ๐ Also support for...of
[Symbol.iterator](): Iterator<Product> {
return this.createIterator();
}
}
// ๐ฆ Cart iterator with filtering
class CartIterator implements Iterator<Product> {
private items: Product[];
private position: number = 0;
constructor(items: Product[]) {
this.items = [...items]; // ๐ก๏ธ Defensive copy
}
hasNext(): boolean {
return this.position < this.items.length;
}
next(): Product {
if (!this.hasNext()) {
throw new Error("๐ No more items in cart!");
}
return this.items[this.position++];
}
current(): Product {
return this.items[this.position];
}
// ๐จ Reset to beginning
reset(): void {
this.position = 0;
console.log("๐ Iterator reset!");
}
}
// ๐ฎ Let's shop!
const cart = new ShoppingCart();
cart.addProduct({ id: "1", name: "TypeScript Book", price: 29.99, emoji: "๐" });
cart.addProduct({ id: "2", name: "Coffee Mug", price: 12.99, emoji: "โ" });
cart.addProduct({ id: "3", name: "Rubber Duck", price: 9.99, emoji: "๐ฆ" });
// ๐ List items with iterator
console.log("๐ Cart contents:");
for (const item of cart) {
console.log(` ${item.emoji} ${item.name} - $${item.price}`);
}
console.log(`๐ฐ Total: $${cart.getTotal().toFixed(2)}`);
๐ฏ Try it yourself: Add a filter iterator that only shows items above a certain price!
๐ฎ Example 2: Game Level Iterator
Letโs make it fun with a game example:
// ๐ Game level structure
interface GameLevel {
id: number;
name: string;
difficulty: "easy" | "medium" | "hard";
stars: number;
emoji: string;
}
// ๐ฎ Level collection with multiple iterators
class GameLevelManager {
private levels: GameLevel[] = [];
// โ Add level
addLevel(level: GameLevel): void {
this.levels.push(level);
console.log(`${level.emoji} Level "${level.name}" added!`);
}
// ๐ฏ Different iteration strategies
allLevels(): LevelIterator {
return new LevelIterator(this.levels);
}
unlockedLevels(starsRequired: number): LevelIterator {
const unlocked = this.levels.filter(level => level.stars <= starsRequired);
return new LevelIterator(unlocked);
}
levelsByDifficulty(difficulty: GameLevel["difficulty"]): LevelIterator {
const filtered = this.levels.filter(level => level.difficulty === difficulty);
return new LevelIterator(filtered);
}
}
// ๐ Advanced iterator with peek functionality
class LevelIterator implements Iterator<GameLevel> {
private levels: GameLevel[];
private index: number = 0;
constructor(levels: GameLevel[]) {
this.levels = levels;
}
hasNext(): boolean {
return this.index < this.levels.length;
}
next(): GameLevel {
if (!this.hasNext()) {
throw new Error("๐ No more levels!");
}
const level = this.levels[this.index++];
console.log(`๐ฎ Loading level: ${level.emoji} ${level.name}`);
return level;
}
current(): GameLevel {
return this.levels[this.index];
}
// ๐ Peek at next without advancing
peek(): GameLevel | null {
if (this.index + 1 < this.levels.length) {
return this.levels[this.index + 1];
}
return null;
}
// ๐ฏ Skip to specific level
skipTo(levelId: number): boolean {
const newIndex = this.levels.findIndex(l => l.id === levelId);
if (newIndex !== -1) {
this.index = newIndex;
return true;
}
return false;
}
// ๐ Make it iterable
[Symbol.iterator](): Iterator<GameLevel> {
return this;
}
}
// ๐ฎ Create game world
const gameManager = new GameLevelManager();
gameManager.addLevel({ id: 1, name: "Tutorial Island", difficulty: "easy", stars: 0, emoji: "๐๏ธ" });
gameManager.addLevel({ id: 2, name: "Forest Path", difficulty: "easy", stars: 10, emoji: "๐ฒ" });
gameManager.addLevel({ id: 3, name: "Mountain Peak", difficulty: "medium", stars: 25, emoji: "๐๏ธ" });
gameManager.addLevel({ id: 4, name: "Dragon's Lair", difficulty: "hard", stars: 50, emoji: "๐ฒ" });
// ๐ Player with 30 stars
const playerStars = 30;
console.log(`\nโจ Unlocked levels (${playerStars} stars):`);
const unlockedIterator = gameManager.unlockedLevels(playerStars);
while (unlockedIterator.hasNext()) {
const level = unlockedIterator.next();
const nextLevel = unlockedIterator.peek();
if (nextLevel) {
console.log(` Next up: ${nextLevel.emoji} ${nextLevel.name}`);
}
}
๐ Advanced Concepts
๐งโโ๏ธ Generator-Based Iterators
When youโre ready to level up, try generators:
// ๐ฏ Using generators for cleaner iterators
class MagicCollection<T> {
private items: T[] = [];
add(item: T): void {
this.items.push(item);
}
// ๐ช Generator function
*[Symbol.iterator](): Generator<T> {
for (const item of this.items) {
console.log("โจ Yielding magic item!");
yield item;
}
}
// ๐ Reverse iterator using generator
*reverseIterator(): Generator<T> {
for (let i = this.items.length - 1; i >= 0; i--) {
yield this.items[i];
}
}
// ๐ซ Filter iterator
*filterIterator(predicate: (item: T) => boolean): Generator<T> {
for (const item of this.items) {
if (predicate(item)) {
yield item;
}
}
}
}
// ๐ฎ Use the magic!
const spells = new MagicCollection<string>();
spells.add("Fireball ๐ฅ");
spells.add("Ice Shield ๐ง");
spells.add("Lightning Bolt โก");
console.log("๐ช Forward iteration:");
for (const spell of spells) {
console.log(` Casting: ${spell}`);
}
console.log("\n๐ Reverse iteration:");
for (const spell of spells.reverseIterator()) {
console.log(` Reversing: ${spell}`);
}
๐๏ธ Composite Iterators
For the brave developers:
// ๐ Iterator that combines multiple iterators
class CompositeIterator<T> implements Iterator<T> {
private iterators: Iterator<T>[];
private currentIndex: number = 0;
constructor(iterators: Iterator<T>[]) {
this.iterators = iterators;
}
hasNext(): boolean {
while (this.currentIndex < this.iterators.length) {
if (this.iterators[this.currentIndex].hasNext()) {
return true;
}
this.currentIndex++;
}
return false;
}
next(): T {
if (!this.hasNext()) {
throw new Error("๐ No more elements in composite!");
}
return this.iterators[this.currentIndex].next();
}
current(): T {
return this.iterators[this.currentIndex].current();
}
}
// ๐จ Paginated iterator for large datasets
class PaginatedIterator<T> implements Iterator<T[]> {
private data: T[];
private pageSize: number;
private currentPage: number = 0;
constructor(data: T[], pageSize: number = 10) {
this.data = data;
this.pageSize = pageSize;
}
hasNext(): boolean {
return this.currentPage * this.pageSize < this.data.length;
}
next(): T[] {
if (!this.hasNext()) {
throw new Error("๐ No more pages!");
}
const start = this.currentPage * this.pageSize;
const end = Math.min(start + this.pageSize, this.data.length);
this.currentPage++;
console.log(`๐ Loading page ${this.currentPage} (items ${start + 1}-${end})`);
return this.data.slice(start, end);
}
current(): T[] {
const start = this.currentPage * this.pageSize;
const end = Math.min(start + this.pageSize, this.data.length);
return this.data.slice(start, end);
}
}
โ ๏ธ Common Pitfalls and Solutions
๐ฑ Pitfall 1: Modifying Collection During Iteration
// โ Wrong way - concurrent modification!
const numbers = new NumberCollection([1, 2, 3, 4, 5]);
const iterator = numbers.createIterator();
while (iterator.hasNext()) {
const num = iterator.next();
if (num === 3) {
numbers.remove(num); // ๐ฅ Danger! Collection modified!
}
}
// โ
Correct way - collect changes, apply after
const toRemove: number[] = [];
const safeIterator = numbers.createIterator();
while (safeIterator.hasNext()) {
const num = safeIterator.next();
if (num === 3) {
toRemove.push(num); // ๐ Note for later
}
}
// ๐ก๏ธ Safe removal after iteration
toRemove.forEach(num => numbers.remove(num));
๐คฏ Pitfall 2: Not Checking hasNext()
// โ Dangerous - might throw error!
function processItems<T>(iterator: Iterator<T>): void {
for (let i = 0; i < 5; i++) {
const item = iterator.next(); // ๐ฅ What if only 3 items?
console.log(item);
}
}
// โ
Safe - always check first!
function processItemsSafely<T>(iterator: Iterator<T>): void {
let count = 0;
while (iterator.hasNext() && count < 5) {
const item = iterator.next(); // โ
Safe now!
console.log(`${count + 1}. ${item}`);
count++;
}
}
๐ ๏ธ Best Practices
- ๐ฏ Keep It Simple: Donโt over-engineer iterators for simple collections
- ๐ Defensive Copying: Create copies of data to prevent external modifications
- ๐ก๏ธ Error Handling: Always check hasNext() before calling next()
- ๐จ Single Responsibility: Each iterator should have one traversal strategy
- โจ Use Generators: For simpler iterator implementations
๐งช Hands-On Exercise
๐ฏ Challenge: Build a Tree Iterator
Create a type-safe tree iterator that can traverse in different orders:
๐ Requirements:
- โ Binary tree structure with generic type
- ๐ท๏ธ Support in-order, pre-order, and post-order traversal
- ๐ค Iterator should be reusable
- ๐ Add level-order (breadth-first) traversal
- ๐จ Each node needs a value and optional emoji!
๐ Bonus Points:
- Add a filter option to skip certain values
- Implement iterator chaining
- Create a reverse iterator for each traversal type
๐ก Solution
๐ Click to see solution
// ๐ฏ Tree node structure
interface TreeNode<T> {
value: T;
emoji?: string;
left?: TreeNode<T>;
right?: TreeNode<T>;
}
// ๐ณ Binary tree with multiple iterators
class BinaryTree<T> {
private root?: TreeNode<T>;
// โ Insert value
insert(value: T, emoji?: string): void {
this.root = this.insertNode(this.root, value, emoji);
console.log(`${emoji || "๐ฟ"} Inserted ${value}`);
}
private insertNode(node: TreeNode<T> | undefined, value: T, emoji?: string): TreeNode<T> {
if (!node) {
return { value, emoji };
}
if (value < node.value) {
node.left = this.insertNode(node.left, value, emoji);
} else {
node.right = this.insertNode(node.right, value, emoji);
}
return node;
}
// ๐ฏ In-order iterator (left -> root -> right)
*inOrderIterator(): Generator<T> {
yield* this.inOrderTraversal(this.root);
}
private *inOrderTraversal(node?: TreeNode<T>): Generator<T> {
if (!node) return;
yield* this.inOrderTraversal(node.left);
console.log(`${node.emoji || "๐"} Visiting: ${node.value}`);
yield node.value;
yield* this.inOrderTraversal(node.right);
}
// ๐ Pre-order iterator (root -> left -> right)
*preOrderIterator(): Generator<T> {
yield* this.preOrderTraversal(this.root);
}
private *preOrderTraversal(node?: TreeNode<T>): Generator<T> {
if (!node) return;
console.log(`${node.emoji || "๐"} Visiting: ${node.value}`);
yield node.value;
yield* this.preOrderTraversal(node.left);
yield* this.preOrderTraversal(node.right);
}
// ๐ Level-order iterator (breadth-first)
*levelOrderIterator(): Generator<T> {
if (!this.root) return;
const queue: TreeNode<T>[] = [this.root];
while (queue.length > 0) {
const node = queue.shift()!;
console.log(`${node.emoji || "๐"} Level visit: ${node.value}`);
yield node.value;
if (node.left) queue.push(node.left);
if (node.right) queue.push(node.right);
}
}
// ๐จ Filtered iterator
*filterIterator(predicate: (value: T) => boolean): Generator<T> {
for (const value of this.inOrderIterator()) {
if (predicate(value)) {
yield value;
}
}
}
// ๐ Default iterator
[Symbol.iterator](): Generator<T> {
return this.inOrderIterator();
}
}
// ๐ฎ Test our tree!
const tree = new BinaryTree<number>();
tree.insert(50, "๐ณ");
tree.insert(30, "๐ฟ");
tree.insert(70, "๐");
tree.insert(20, "๐ฑ");
tree.insert(40, "๐");
tree.insert(60, "๐พ");
tree.insert(80, "๐ฒ");
console.log("\n๐ฏ In-order traversal:");
for (const value of tree.inOrderIterator()) {
console.log(` Value: ${value}`);
}
console.log("\n๐ Pre-order traversal:");
for (const value of tree.preOrderIterator()) {
console.log(` Value: ${value}`);
}
console.log("\n๐ Level-order traversal:");
for (const value of tree.levelOrderIterator()) {
console.log(` Value: ${value}`);
}
console.log("\n๐จ Filtered (> 40):");
for (const value of tree.filterIterator(v => v > 40)) {
console.log(` Value: ${value}`);
}
๐ Key Takeaways
Youโve learned so much! Hereโs what you can now do:
- โ Create custom iterators with confidence ๐ช
- โ Use Symbol.iterator for built-in iteration support ๐ก๏ธ
- โ Implement generators for cleaner code ๐ฏ
- โ Handle edge cases safely ๐
- โ Build traversal strategies for any collection! ๐
Remember: Iterators provide a clean, consistent way to traverse collections without exposing internals! ๐ค
๐ค Next Steps
Congratulations! ๐ Youโve mastered the Iterator Pattern!
Hereโs what to do next:
- ๐ป Practice with the tree iterator exercise
- ๐๏ธ Add iterators to your existing collections
- ๐ Explore the Visitor Pattern for processing iterated elements
- ๐ Share your custom iterator implementations!
Remember: Every iteration brings you closer to mastery. Keep coding, keep learning, and most importantly, have fun! ๐
Happy coding! ๐๐โจ