typescript
52 lines · 7 steps
Building a lazy iterator pipeline in TypeScript
Generator functions and a chainable wrapper let you compose map, filter, and take without ever building intermediate arrays.
Explained by
highlit
1function* map<T, U>(source: Iterable<T>, fn: (value: T, index: number) => U): Generator<U> {
2 let index = 0;
3 for (const value of source) {
4 yield fn(value, index++);
5 }
6}
7
8function* filter<T>(source: Iterable<T>, predicate: (value: T, index: number) => boolean): Generator<T> {
9 let index = 0;
10 for (const value of source) {
11 if (predicate(value, index++)) {
12 yield value;
13 }
14 }
15}
16
17function* take<T>(source: Iterable<T>, count: number): Generator<T> {
18 if (count <= 0) return;
19 let taken = 0;
20 for (const value of source) {
21 yield value;
22 if (++taken >= count) return;
23 }
24}
25
26class Lazy<T> implements Iterable<T> {
27 constructor(private readonly source: Iterable<T>) {}
28
29 static from<T>(source: Iterable<T>): Lazy<T> {
30 return new Lazy(source);
31 }
32
33 map<U>(fn: (value: T, index: number) => U): Lazy<U> {
34 return new Lazy(map(this.source, fn));
35 }
36
37 filter(predicate: (value: T, index: number) => boolean): Lazy<T> {
38 return new Lazy(filter(this.source, predicate));
39 }
40
41 take(count: number): Lazy<T> {
42 return new Lazy(take(this.source, count));
43 }
44
45 toArray(): T[] {
46 return [...this.source];
47 }
48
49 [Symbol.iterator](): Iterator<T> {
50 return this.source[Symbol.iterator]();
51 }
52}
01 / 01
STEP 01
‹ swipe to step through ›
Walkthrough
Space play
←→ step
click any line
Three takeaways
- 1Generators produce values on demand, so chained operations run element-by-element instead of materializing each stage.
- 2Wrapping generators in a class with methods that return new instances gives you a fluent, composable pipeline API.
- 3Implementing Symbol.iterator makes a class a first-class iterable that spreads, destructures, and loops like native collections.
Related explainers
typescript
import { registerLocaleData } from '@angular/common'; import localeFr from '@angular/common/locales/fr'; import localeFrExtra from '@angular/common/locales/extra/fr'; import localeDe from '@angular/common/locales/de';
Locale-aware bootstrapping in Angular
i18n
localization
dependency-injection
Intermediate
8 steps
typescript
import { Module } from '@nestjs/common'; import { ConfigModule } from '@nestjs/config'; import * as Joi from 'joi';
Validating env config at boot in NestJS
configuration
schema-validation
environment-variables
Intermediate
8 steps
typescript
import { Inject, Injectable, Logger } from '@nestjs/common'; import { CACHE_MANAGER } from '@nestjs/cache-manager'; import { Cache } from 'cache-manager'; import { InjectRepository } from '@nestjs/typeorm';
A cache-aside country lookup in NestJS
cache-aside
dependency-injection
batch-lookup
Intermediate
8 steps
rust
use std::f64::consts::PI; #[derive(Debug, Clone, Copy)] pub struct LatLng {
Building geographic bounding boxes in Rust
geospatial
value-types
option
Intermediate
7 steps
typescript
import { Injectable, effect, signal, computed } from '@angular/core'; interface Preferences { theme: 'light' | 'dark';
A signal-based preferences store in Angular
signals
state-management
persistence
Intermediate
7 steps
python
import random from typing import Iterator, List
How reservoir sampling picks k items
reservoir-sampling
streaming
randomness
Intermediate
5 steps
Share this explainer
Here's the card — post it anywhere.
Made with highlit — turn any snippet into a walkthrough like this in about a minute.
Explain your code
Embed this explainer
Drop the interactive walkthrough into a blog or docs. Views never cost a credit.
<iframe src="https://highlit.co/explainers/building-a-lazy-iterator-pipeline-in-typescript-explained-typescript-a503/embed?autoplay=1" width="100%" height="520" loading="lazy" style="border:0"></iframe>
Autoplay is on by default — add ?autoplay=0 to start paused.