Advanced Iterator Patterns
Internal vs External Iterators
There are two fundamental approaches to iteration: internal and external. Each has distinct advantages and use cases.
External Iterators
With external iterators, the client controls the iteration process:
typescript1interface ExternalIterator<T> {2 next(): IteratorResult<T>;3 hasNext(): boolean;4}56class RouteIterator implements ExternalIterator<DeliveryStop> {7 private position = 0;89 constructor(private stops: DeliveryStop[]) {}1011 hasNext(): boolean {12 return this.position < this.stops.length;13 }1415 next(): IteratorResult<DeliveryStop> {16 if (this.hasNext()) {17 return {18 done: false,19 value: this.stops[this.position++]20 };21 }22 return { done: true, value: undefined as any };23 }24}2526// Client controls iteration27const iterator = new RouteIterator(stops);28while (iterator.hasNext()) {29 const stop = iterator.next().value;3031 // Client decides when to continue32 if (stop.priority === 'high') {33 processImmediately(stop);34 } else {35 // Can skip or delay processing36 break;37 }38}
Advantages:
- Client has full control over iteration flow
- Can pause, resume, or stop iteration
- Can maintain multiple iterators simultaneously
- Can compare positions across iterators
Disadvantages:
- More verbose client code
- Client must manage iterator state
Internal Iterators
With internal iterators, the collection controls the iteration:
typescript1class DeliveryRoute {2 private stops: DeliveryStop[] = [];34 // Internal iterator5 forEach(callback: (stop: DeliveryStop, index: number) => void | boolean): void {6 for (let i = 0; i < this.stops.length; i++) {7 const result = callback(this.stops[i], i);8 // Allow early termination by returning false9 if (result === false) break;10 }11 }1213 // Filter with internal iteration14 filter(predicate: (stop: DeliveryStop) => boolean): DeliveryStop[] {15 const results: DeliveryStop[] = [];16 this.forEach((stop) => {17 if (predicate(stop)) {18 results.push(stop);19 }20 });21 return results;22 }23}2425// Collection controls iteration26route.forEach((stop, index) => {27 console.log(`Stop ${index}: ${stop.address}`);28});
Advantages:
- Simpler client code
- Collection can optimize traversal
- Easier to parallelize
Disadvantages:
- Less control for client
- Harder to pause or compare iterators
- May process more than needed
Hybrid Approach: Generators
TypeScript generators provide the best of both worlds:
typescript1class DeliveryRoute implements Iterable<DeliveryStop> {2 private stops: DeliveryStop[] = [];34 // External-style: client controls5 *[Symbol.iterator](): Generator<DeliveryStop> {6 yield* this.stops;7 }89 // Internal-style: collection controls10 forEach(callback: (stop: DeliveryStop) => void): void {11 for (const stop of this) {12 callback(stop);13 }14 }15}
Filtering Iterators
Filtering iterators apply predicates during iteration without creating intermediate arrays.
Basic Filtering Iterator
typescript1class FilteringIterator<T> implements Iterator<T> {2 private position = 0;34 constructor(5 private items: T[],6 private predicate: (item: T) => boolean7 ) {}89 next(): IteratorResult<T> {10 // Skip items that don't match predicate11 while (this.position < this.items.length) {12 const item = this.items[this.position++];13 if (this.predicate(item)) {14 return { done: false, value: item };15 }16 }17 return { done: true, value: undefined as any };18 }19}
Generator-Based Filtering
typescript1class DeliveryRoute {2 private stops: DeliveryStop[] = [];34 *filterStops(predicate: (stop: DeliveryStop) => boolean): Generator<DeliveryStop> {5 for (const stop of this.stops) {6 if (predicate(stop)) {7 yield stop;8 }9 }10 }1112 // Specific filters13 *byPriority(priority: string): Generator<DeliveryStop> {14 yield* this.filterStops(stop => stop.priority === priority);15 }1617 *byZone(zone: string): Generator<DeliveryStop> {18 yield* this.filterStops(stop => stop.zone === zone);19 }2021 *incompleteStops(): Generator<DeliveryStop> {22 yield* this.filterStops(stop => !stop.completed);23 }24}2526// Usage27const route = new DeliveryRoute();2829// Only iterate incomplete high-priority stops30for (const stop of route.filterStops(s => s.priority === 'high' && !s.completed)) {31 deliverUrgently(stop);32}
Chaining Filters
typescript1class IteratorChain<T> {2 constructor(private source: Iterable<T>) {}34 *filter(predicate: (item: T) => boolean): Generator<T> {5 for (const item of this.source) {6 if (predicate(item)) {7 yield item;8 }9 }10 }1112 *map<U>(mapper: (item: T) => U): Generator<U> {13 for (const item of this.source) {14 yield mapper(item);15 }16 }1718 *take(count: number): Generator<T> {19 let taken = 0;20 for (const item of this.source) {21 if (taken >= count) break;22 yield item;23 taken++;24 }25 }26}2728// Usage29const route = new DeliveryRoute();30const chain = new IteratorChain(route)31 .filter(stop => stop.zone === 'A')32 .filter(stop => stop.priority === 'high')33 .take(5);3435for (const stop of chain) {36 console.log(stop.address);37}
Lazy Evaluation
Lazy evaluation defers computation until values are actually needed, improving performance for large datasets.
Eager vs Lazy
typescript1class PackageCollection {2 private packages: Package[] = [];34 // ❌ Eager: processes everything immediately5 getHighPriorityPackages(): Package[] {6 return this.packages7 .filter(pkg => pkg.priority === 'high')8 .map(pkg => this.enrichPackage(pkg))9 .slice(0, 10);10 // Processes ALL packages, even though we only need 1011 }1213 // ✅ Lazy: processes only what's needed14 *getHighPriorityPackagesLazy(): Generator<Package> {15 let count = 0;16 for (const pkg of this.packages) {17 if (pkg.priority === 'high') {18 yield this.enrichPackage(pkg);19 if (++count >= 10) break; // Stop after 1020 }21 }22 }2324 private enrichPackage(pkg: Package): Package {25 // Expensive operation26 return { ...pkg, route: calculateOptimalRoute(pkg) };27 }28}
Lazy Database Queries
typescript1class PackageRepository {2 // Lazy loading from database3 async *findPackages(criteria: PackageCriteria): AsyncGenerator<Package> {4 let offset = 0;5 const batchSize = 100;67 while (true) {8 // Fetch one batch at a time9 const batch = await this.queryDatabase({10 ...criteria,11 offset,12 limit: batchSize13 });1415 if (batch.length === 0) break;1617 // Yield each package18 for (const pkg of batch) {19 yield pkg;20 }2122 offset += batchSize;2324 // If batch is smaller than batchSize, we've reached the end25 if (batch.length < batchSize) break;26 }27 }2829 private async queryDatabase(query: any): Promise<Package[]> {30 // Database query implementation31 return [];32 }33}3435// Usage: only loads what you process36const repo = new PackageRepository();3738for await (const pkg of repo.findPackages({ zone: 'A' })) {39 await processPackage(pkg);4041 // Can stop early without loading remaining data42 if (pkg.isUrgent) {43 await escalate(pkg);44 break; // Remaining packages never queried45 }46}
Lazy Transformations
typescript1class LazySequence<T> {2 constructor(private source: Iterable<T>) {}34 // All transformations are lazy5 *filter(predicate: (item: T) => boolean): Generator<T> {6 for (const item of this.source) {7 if (predicate(item)) {8 yield item;9 }10 }11 }1213 *map<U>(mapper: (item: T) => U): Generator<U> {14 for (const item of this.source) {15 yield mapper(item);16 }17 }1819 *flatMap<U>(mapper: (item: T) => Iterable<U>): Generator<U> {20 for (const item of this.source) {21 yield* mapper(item);22 }23 }2425 // Terminal operations that trigger evaluation26 toArray(): T[] {27 return Array.from(this.source);28 }2930 first(): T | undefined {31 for (const item of this.source) {32 return item; // Only processes until first item33 }34 return undefined;35 }3637 reduce<U>(reducer: (acc: U, item: T) => U, initial: U): U {38 let acc = initial;39 for (const item of this.source) {40 acc = reducer(acc, item);41 }42 return acc;43 }44}4546// Usage: transformations don't execute until terminal operation47const packages = new LazySequence(allPackages)48 .filter(pkg => pkg.weight > 10) // Not executed yet49 .map(pkg => calculateShipping(pkg)) // Not executed yet50 .filter(pkg => pkg.shippingCost < 50) // Not executed yet51 .first(); // NOW executes, stops at first match
Infinite Iterators
Infinite iterators generate values indefinitely, useful for IDs, sequences, or event streams.
ID Generators
typescript1function* generateTrackingIds(prefix: string): Generator<string> {2 let counter = 1;3 while (true) {4 yield `${prefix}-${Date.now()}-${counter.toString().padStart(6, '0')}`;5 counter++;6 }7}89// Usage10const idGenerator = generateTrackingIds('PKG');11const id1 = idGenerator.next().value; // PKG-1734820800000-00000112const id2 = idGenerator.next().value; // PKG-1734820800000-000002
Fibonacci Sequence
typescript1function* fibonacci(): Generator<number> {2 let [prev, curr] = [0, 1];3 while (true) {4 yield curr;5 [prev, curr] = [curr, prev + curr];6 }7}89// Take first 10 Fibonacci numbers10const fibs = [];11const fibGen = fibonacci();12for (let i = 0; i < 10; i++) {13 fibs.push(fibGen.next().value);14}15console.log(fibs); // [1, 1, 2, 3, 5, 8, 13, 21, 34, 55]
Event Stream Iterator
typescript1class RouteUpdateStream {2 private listeners: ((update: RouteUpdate) => void)[] = [];3 private updates: RouteUpdate[] = [];45 emit(update: RouteUpdate): void {6 this.updates.push(update);7 this.listeners.forEach(listener => listener(update));8 }910 async *[Symbol.asyncIterator](): AsyncGenerator<RouteUpdate> {11 let position = 0;1213 while (true) {14 // Yield existing updates15 while (position < this.updates.length) {16 yield this.updates[position++];17 }1819 // Wait for new update20 await new Promise<void>(resolve => {21 const listener = () => {22 this.listeners = this.listeners.filter(l => l !== listener);23 resolve();24 };25 this.listeners.push(listener);26 });27 }28 }29}3031// Usage32const stream = new RouteUpdateStream();3334(async () => {35 for await (const update of stream) {36 console.log('Route updated:', update);37 }38})();3940// Emit updates41stream.emit({ routeId: 'R1', status: 'in-progress' });42stream.emit({ routeId: 'R1', status: 'completed' });
Bidirectional Iteration
Some collections benefit from traversing in both directions.
Bidirectional Iterator Interface
typescript1interface BidirectionalIterator<T> extends Iterator<T> {2 next(): IteratorResult<T>;3 previous(): IteratorResult<T>;4 hasNext(): boolean;5 hasPrevious(): boolean;6}78class DeliveryRouteIterator implements BidirectionalIterator<DeliveryStop> {9 private position = 0;1011 constructor(private stops: DeliveryStop[]) {}1213 hasNext(): boolean {14 return this.position < this.stops.length;15 }1617 hasPrevious(): boolean {18 return this.position > 0;19 }2021 next(): IteratorResult<DeliveryStop> {22 if (this.hasNext()) {23 return {24 done: false,25 value: this.stops[this.position++]26 };27 }28 return { done: true, value: undefined as any };29 }3031 previous(): IteratorResult<DeliveryStop> {32 if (this.hasPrevious()) {33 return {34 done: false,35 value: this.stops[--this.position]36 };37 }38 return { done: true, value: undefined as any };39 }4041 // Jump to position42 seek(position: number): void {43 if (position >= 0 && position <= this.stops.length) {44 this.position = position;45 }46 }4748 // Reset to beginning49 reset(): void {50 this.position = 0;51 }5253 // Reset to end54 resetToEnd(): void {55 this.position = this.stops.length;56 }57}5859// Usage60const iterator = new DeliveryRouteIterator(stops);6162// Move forward63console.log(iterator.next().value); // Stop 164console.log(iterator.next().value); // Stop 26566// Move backward67console.log(iterator.previous().value); // Stop 168console.log(iterator.previous().value); // Stop 06970// Jump to position71iterator.seek(5);72console.log(iterator.next().value); // Stop 5
Doubly-Linked List Iterator
typescript1class LinkedListNode<T> {2 constructor(3 public value: T,4 public next: LinkedListNode<T> | null = null,5 public prev: LinkedListNode<T> | null = null6 ) {}7}89class DoublyLinkedList<T> implements Iterable<T> {10 private head: LinkedListNode<T> | null = null;11 private tail: LinkedListNode<T> | null = null;1213 append(value: T): void {14 const node = new LinkedListNode(value);15 if (!this.tail) {16 this.head = this.tail = node;17 } else {18 node.prev = this.tail;19 this.tail.next = node;20 this.tail = node;21 }22 }2324 // Forward iterator25 *[Symbol.iterator](): Generator<T> {26 let current = this.head;27 while (current) {28 yield current.value;29 current = current.next;30 }31 }3233 // Backward iterator34 *reverse(): Generator<T> {35 let current = this.tail;36 while (current) {37 yield current.value;38 current = current.prev;39 }40 }4142 createBidirectionalIterator(): BidirectionalIterator<T> {43 let current = this.head;4445 return {46 hasNext: () => current !== null && current.next !== null,47 hasPrevious: () => current !== null && current.prev !== null,4849 next: () => {50 if (current && current.next) {51 current = current.next;52 return { done: false, value: current.value };53 }54 return { done: true, value: undefined as any };55 },5657 previous: () => {58 if (current && current.prev) {59 current = current.prev;60 return { done: false, value: current.value };61 }62 return { done: true, value: undefined as any };63 }64 };65 }66}6768// Usage69const list = new DoublyLinkedList<string>();70list.append('A');71list.append('B');72list.append('C');7374// Forward iteration75for (const item of list) {76 console.log(item); // A, B, C77}7879// Backward iteration80for (const item of list.reverse()) {81 console.log(item); // C, B, A82}
Composite Iterators
Composite iterators combine multiple iterators into a single traversal.
Merging Multiple Routes
typescript1class RouteCollection implements Iterable<DeliveryStop> {2 private routes: DeliveryRoute[] = [];34 addRoute(route: DeliveryRoute): void {5 this.routes.push(route);6 }78 // Iterate all stops across all routes9 *[Symbol.iterator](): Generator<DeliveryStop> {10 for (const route of this.routes) {11 yield* route;12 }13 }1415 // Merge by priority across all routes16 *byPriority(priority: string): Generator<DeliveryStop> {17 for (const route of this.routes) {18 yield* route.byPriority(priority);19 }20 }2122 // Interleave stops from multiple routes (round-robin)23 *interleave(): Generator<DeliveryStop> {24 const iterators = this.routes.map(r => r[Symbol.iterator]());25 let activeIterators = iterators.length;2627 while (activeIterators > 0) {28 for (let i = 0; i < iterators.length; i++) {29 const result = iterators[i].next();30 if (!result.done) {31 yield result.value;32 } else if (activeIterators > 0) {33 activeIterators--;34 }35 }36 }37 }38}3940// Usage41const collection = new RouteCollection();42collection.addRoute(route1);43collection.addRoute(route2);44collection.addRoute(route3);4546// All stops from all routes47for (const stop of collection) {48 console.log(stop.address);49}5051// High priority stops from all routes52for (const stop of collection.byPriority('high')) {53 priorityDelivery(stop);54}
Best Practices
1. Choose the Right Iterator Type
- External: When you need fine-grained control
- Internal: When you want simplicity and collection optimization
- Generator: For most cases, provides flexibility and clarity
2. Prefer Lazy Evaluation
typescript1// ✅ Lazy: efficient for large datasets2*processLarge(): Generator<Result> {3 for (const item of this.items) {4 yield transform(item);5 }6}78// ❌ Eager: processes everything upfront9processAll(): Result[] {10 return this.items.map(item => transform(item));11}
3. Use Filtering Iterators
typescript1// ✅ Filter during iteration2*filterActive(): Generator<Stop> {3 for (const stop of this.stops) {4 if (!stop.completed) yield stop;5 }6}78// ❌ Create intermediate array9getActiveStops(): Stop[] {10 return this.stops.filter(s => !s.completed);11}
4. Implement Bidirectional When Needed
Only implement bidirectional iteration when it provides clear value (undo/redo, navigation history, etc.).
Key Takeaways
- External iterators give clients full control over iteration
- Internal iterators simplify client code but limit control
- Filtering iterators avoid creating intermediate collections
- Lazy evaluation defers computation until values are needed
- Infinite iterators generate values indefinitely
- Bidirectional iterators enable forward and backward traversal
- Composite iterators combine multiple iterators into one
Next Steps
In the upcoming workshop, you'll implement a complete delivery route iterator system with:
- Iterable delivery routes
- Filtering by zone and priority
- Lazy evaluation with generators
- Advanced traversal patterns
These advanced iterator patterns will help you build efficient, flexible data traversal systems for complex logistics operations.