TypeScript Iterators
last modified October 15, 2026
An iterator is an object that produces a sequence of values, one at
a time. JavaScript defines two protocols for this. An iterator has a
next method that returns objects of the form
{ value, done }. An iterable is an object with a
[Symbol.iterator] method that returns an iterator. Arrays,
strings, maps, and sets are iterable, and so is any object that implements
the protocol. TypeScript describes the protocols with the
Iterator<T> and Iterable<T> types.
Basic Iterator Protocol
Calling the [Symbol.iterator] method of an array
returns an iterator. Each call to next returns the next
value.
const numbers = [1, 2, 3]; const iterator = numbers[Symbol.iterator](); console.log(iterator.next()); console.log(iterator.next()); console.log(iterator.next()); console.log(iterator.next());
The first three calls return the elements with
done: false. The fourth call signals the end of the sequence with
done: true and an undefined value. The
for...of loop does exactly this behind the scenes: it calls
next until done is true.
$ npx tsx basic_iterator.ts
{ value: 1, done: false }
{ value: 2, done: false }
{ value: 3, done: false }
{ value: undefined, done: true }
Consuming Iterables
Besides for...of, several other language features
accept any iterable.
const word = "hey"; const unique = new Set([3, 1, 3, 2, 1]); const letters = [...word]; const sorted = Array.from(unique).sort(); const [first, ...rest] = unique; console.log(letters); console.log(sorted); console.log(first, rest); console.log(Math.max(...unique));
The spread operator, Array.from, and array
destructuring all iterate over their argument. They work the same way for a
string, a set, or a custom iterable. TypeScript infers the element types:
letters is a string[] and first a
number.
$ npx tsx consuming_iterables.ts [ 'h', 'e', 'y' ] [ 1, 2, 3 ] 3 [ 1, 2 ] 3
Custom Iterators
A class becomes iterable by implementing the
[Symbol.iterator] method. The implements
Iterable<number> clause lets the compiler check the
method.
class NumberRange implements Iterable<number> {
constructor(private start: number, private end: number, private step = 1) {}
[Symbol.iterator](): Iterator<number> {
let current = this.start;
return {
next: (): IteratorResult<number> => {
if (current <= this.end) {
const value = current;
current += this.step;
return { value, done: false };
}
return { value: undefined, done: true };
},
};
}
}
const range = new NumberRange(1, 10, 3);
for (const num of range) {
console.log(num);
}
console.log([...range]);
Each call to [Symbol.iterator] creates a new iterator
object with its own current variable. That is why the range can
be iterated twice: once with the loop and once with the spread operator. The
IteratorResult<number> return type covers both kinds of
results, the values with done: false and the final one with
done: true.
$ npx tsx custom_iterator.ts 1 4 7 10 [ 1, 4, 7, 10 ]
Generators
Writing the iterator object by hand is verbose. A generator
function, declared with function*, creates iterators
automatically.
function* generateSequence(start: number, end: number): Generator<number> {
for (let i = start; i <= end; i++) {
yield i;
}
}
for (const num of generateSequence(1, 3)) {
console.log(num);
}
class Countdown implements Iterable<number> {
constructor(private from: number) {}
*[Symbol.iterator](): Generator<number> {
for (let i = this.from; i > 0; i--) {
yield i;
}
}
}
console.log([...new Countdown(5)]);
Each yield produces one value and pauses the function
until the next value is requested. The Countdown class shows the
most convenient way to make a class iterable: a generator method named
*[Symbol.iterator]. See the generators
tutorial for more details.
$ npx tsx generator.ts 1 2 3 [ 5, 4, 3, 2, 1 ]
One-Shot Iterators
An iterable creates a new iterator each time. An iterator itself, such as the object returned by a generator function, can be consumed only once.
function* letters(): Generator<string> {
yield "a";
yield "b";
yield "c";
}
const gen = letters();
console.log([...gen]);
console.log([...gen]);
const array = ["a", "b", "c"];
console.log([...array]);
console.log([...array]);
The second spread of gen produces an empty array,
because the generator has already finished. The array can be spread any
number of times, because every spread asks it for a fresh iterator. To
iterate generated values several times, call the generator function again or
store the values in an array.
$ npx tsx one_shot.ts [ 'a', 'b', 'c' ] [] [ 'a', 'b', 'c' ] [ 'a', 'b', 'c' ]
Iterating Over Maps
Maps are iterable and provide three iterators: over keys, over values, and over entries.
const stock = new Map<string, number>([
["apples", 5],
["pears", 0],
["plums", 12],
]);
for (const [fruit, count] of stock) {
console.log(`${fruit}: ${count}`);
}
console.log([...stock.keys()]);
console.log([...stock.values()].reduce((a, b) => a + b, 0));
const available = [...stock].filter(([, count]) => count > 0).map(([fruit]) => fruit);
console.log(available);
Iterating the map directly is the same as iterating
stock.entries(): each element is a [key, value]
pair, which the loop destructures. The keys and
values methods return separate iterators. Maps keep the
insertion order. Spreading a map into an array of pairs makes array methods
such as filter and map available.
$ npx tsx map_iterator.ts apples: 5 pears: 0 plums: 12 [ 'apples', 'pears', 'plums' ] 17 [ 'apples', 'plums' ]
Iterating Over Sets
Sets are iterable as well. They store each value only once.
const visits = ["home", "about", "home", "blog", "about", "home"];
const pages = new Set(visits);
for (const page of pages) {
console.log(page);
}
console.log(pages.size, [...pages].join(", "));
Creating a set from an array removes duplicates while keeping the
order of the first occurrences. The size property returns the
number of unique values. The type of pages is
Set<string>, inferred from the array.
$ npx tsx set_iterator.ts home about blog 3 home, about, blog
Lazy and Infinite Sequences
Iterators compute values only when they are requested. This makes it possible to describe infinite sequences and take only as many values as needed.
function* naturals(): Generator<number> {
let n = 1;
while (true) {
yield n++;
}
}
function* take<T>(source: Iterable<T>, count: number): Generator<T> {
if (count <= 0) {
return;
}
let taken = 0;
for (const item of source) {
yield item;
if (++taken >= count) {
return;
}
}
}
function* filter<T>(source: Iterable<T>, predicate: (item: T) => boolean): Generator<T> {
for (const item of source) {
if (predicate(item)) {
yield item;
}
}
}
const squares = filter(naturals(), n => Number.isInteger(Math.sqrt(n)));
console.log([...take(squares, 5)]);
The naturals generator never ends, yet the program
finishes, because take stops after five values. The
filter and take helpers work with any iterable, and
TypeScript infers their type parameter T as
number here. Each value passes through the whole chain before
the next one is generated.
$ npx tsx lazy_sequence.ts [ 1, 4, 9, 16, 25 ]
Async Iterators
Async iterators produce values that become available over time,
such as pages of results from an API. They are created with async generators
and consumed with for await...of.
type Page = { page: number; items: string[] };
function delay(ms: number): Promise<void> {
return new Promise(resolve => setTimeout(resolve, ms));
}
async function fetchPage(page: number): Promise<Page> {
await delay(50);
return { page, items: [`item ${page * 2 - 1}`, `item ${page * 2}`] };
}
async function* fetchAll(pages: number): AsyncGenerator<string> {
for (let page = 1; page <= pages; page++) {
const result = await fetchPage(page);
yield* result.items;
}
}
async function main(): Promise<void> {
for await (const item of fetchAll(3)) {
console.log(item);
}
}
main();
The fetchPage function simulates a slow API call. The
fetchAll async generator requests one page at a time and yields
its items individually; yield* delegates to the items array. The
caller sees a simple stream of strings and does not need to know about pages.
The return type AsyncGenerator<string> makes
item a string.
$ npx tsx async_iterator.ts item 1 item 2 item 3 item 4 item 5 item 6
Combining Iterators
Generators can delegate to other iterables with yield*.
This makes it easy to combine sources into one sequence.
function* concat<T>(...sources: Iterable<T>[]): Generator<T> {
for (const source of sources) {
yield* source;
}
}
function* zip<A, B>(a: Iterable<A>, b: Iterable<B>): Generator<[A, B]> {
const itA = a[Symbol.iterator]();
const itB = b[Symbol.iterator]();
while (true) {
const x = itA.next();
const y = itB.next();
if (x.done || y.done) {
return;
}
yield [x.value, y.value];
}
}
console.log([...concat([1, 2], new Set([3, 4]), [5])]);
for (const [name, score] of zip(["Alice", "Bob", "Carol"], [92, 78])) {
console.log(`${name}: ${score}`);
}
The concat function chains any number of iterables of
the same type; here an array, a set, and another array. The
zip function walks two iterables in parallel by calling
next on both iterators and stops when the shorter one ends, so
Carol, who has no score, is not printed. The yielded pairs have the tuple
type [string, number].
$ npx tsx combine_iterators.ts [ 1, 2, 3, 4, 5 ] Alice: 92 Bob: 78
Best Practices
- Implement Iterable, Not Just Iterator: Return a new iterator from
[Symbol.iterator]so that an object can be iterated more than once. - Prefer Generators: A generator method is shorter and less error-prone than a hand-written
nextmethod. - Use Laziness Deliberately: Generators avoid building large intermediate arrays, but an infinite sequence must always be limited by the consumer.
- Type the Results: Annotate generators with
Generator<T>orAsyncGenerator<T>to document the element type. - Remember One-Shot Iterators: A generator object can be consumed only once; store the values if they are needed again.
Source
TypeScript Handbook: Iterators and Generators
This tutorial covered iterators in TypeScript. Any object that
implements Symbol.iterator works with for...of,
spreading, and destructuring, and generators are the simplest way to write
one.
Author
List all TypeScript tutorials.