Creating Custom Iterators and Iterables in JavaScript

Serhii Koziy
3 min readJan 21, 2025

As a Senior Frontend Developer, you’re likely already familiar with JavaScript’s iteration mechanisms, such as for...of, forEach, and the spread operator. These tools rely on the iterable protocol, a fundamental concept that allows objects to define custom iteration behavior. But what if you need to iterate over a custom data structure in a specific way? That’s where creating custom iterators and iterables comes into play. Let’s dive into what iterators and iterables are, and how to implement them using JavaScript and TypeScript.

What Are Iterables and Iterators?

An iterable is an object that defines its iteration behavior, typically using a Symbol.iterator method. This method returns an iterator, which is an object responsible for producing a sequence of values, one at a time.

  • Iterable Protocol: An object is iterable if it has a Symbol.iterator method that returns an iterator.
  • Iterator Protocol: An iterator is an object with a next method that returns an object containing two properties:
  • value: The current value in the sequence.
  • done: A boolean indicating whether the iteration is complete.

Why Create Custom Iterators and Iterables?

JavaScript’s built-in objects like arrays, maps, and sets are iterable by default. However, for custom data structures or specific iteration requirements, defining custom iterables can provide:

  1. Flexibility: Tailored iteration logic for your data structure.
  2. Encapsulation: Keep iteration logic encapsulated within the object itself.
  3. Improved Readability: Simplify code by abstracting complex iteration logic.

Implementing a Custom Iterator

Let’s start with a simple example: creating a custom iterator for a range of numbers.

Example 1: Basic Custom Iterator

function createRangeIterator(start, end) {
let current = start;
return {
next() {
if (current <= end) {
return { value: current++, done: false };
}
return { value: undefined, done: true };
},
};
}

const range = createRangeIterator(1, 5);
console.log(range.next()); // { value: 1, done: false }
console.log(range.next()); // { value: 2, done: false }
console.log(range.next()); // { value: 3, done: false }

Creating an Iterable Object

To make an object iterable, we implement the Symbol.iterator method, which must return an iterator.

Example 2: Adding Iterable Protocol

function createRange(start, end) {
return {
[Symbol.iterator]() {
return createRangeIterator(start, end);
},
};
}

const range = createRange(1, 5);
for (const num of range) {
console.log(num); // 1, 2, 3, 4, 5
}

Using Generators for Simplicity

Generators simplify creating iterators by automatically handling the state and next method.

Example 3: Custom Iterable with Generators

function createRange(start, end) {
return {
*[Symbol.iterator]() {
for (let i = start; i <= end; i++) {
yield i;
}
},
};
}

const range = createRange(1, 5);
console.log([...range]); // [1, 2, 3, 4, 5]

Custom Iterables in TypeScript

TypeScript enhances the developer experience by providing type safety and autocompletion for iterables and iterators.

Example 4: TypeScript Iterable Example

type RangeIteratorResult = { value: number; done: boolean };

function createRangeIterator(start: number, end: number): Iterator<number> {
let current = start;
return {
next(): RangeIteratorResult {
if (current <= end) {
return { value: current++, done: false };
}
return { value: undefined as any, done: true };
},
};
}
function createRange(start: number, end: number): Iterable<number> {
return {
[Symbol.iterator](): Iterator<number> {
return createRangeIterator(start, end);
},
};
}
const range = createRange(1, 5);
for (const num of range) {
console.log(num); // 1, 2, 3, 4, 5
}

Advanced Use Case: Bidirectional Iterator

Let’s build a custom bidirectional iterator to iterate both forward and backward.

Example 5: Bidirectional Iterator

function createBidirectionalRange(start: number, end: number): Iterable<number> {
let current = start;
return {
[Symbol.iterator]() {
return {
next(): IteratorResult<number> {
if (current <= end) {
return { value: current++, done: false };
}
return { value: undefined as any, done: true };
},

previous(): IteratorResult<number> {
if (current > start) {
return { value: --current, done: false };
}
return { value: undefined as any, done: true };
},
};
},
};
}

Conclusion

Creating custom iterators and iterables allows you to define how your data structures are traversed. By adhering to the iterable and iterator protocols, you can create intuitive, reusable, and encapsulated iteration logic. Whether you’re working with JavaScript or TypeScript, these tools enhance your ability to build robust and maintainable applications. Leverage custom iterables to simplify complex iteration scenarios and provide a seamless experience for future developers using your code.

Sign up to discover human stories that deepen your understanding of the world.

Free

Distraction-free reading. No ads.

Organize your knowledge with lists and highlights.

Tell your story. Find your audience.

Membership

Read member-only stories

Support writers you read most

Earn money for your writing

Listen to audio narrations

Read offline with the Medium app

No responses yet

Write a response