Skip to content

Iterator Helpers Bring Array Methods to Any Iterable

JavaScript's iterator helpers add map, filter, take, and more directly to any iterable, without the memory cost of building an intermediate array first.

· · 5 min read

Quick Take

I used to convert every custom iterator to an array just so I could call .filter() on it. Iterator helpers, now baseline across browsers and Node, end that workaround for good.

Generators are one of JavaScript's most underused features, partly because working with them used to mean writing a manual for...of loop for every transformation, or converting to an array and losing the laziness that made a generator worth using in the first place. Iterator helpers fix that by giving iterators the same fluent API arrays have had since forever.

TL;DR: Iterator helpers add .map(), .filter(), .take(), .drop(), .flatMap(), .reduce(), and .toArray() directly to any iterator or generator, no array conversion required. They're lazy, so you can chain transformations over an infinite generator and only compute what you actually consume. Support landed across Chrome, Firefox, Safari, and Node.js 22+ through 2025, and it's Baseline as of 2026.

The Problem Iterator Helpers Solve

Say you have a generator that produces an infinite sequence, and you want the first five even numbers, squared:

function* naturalNumbers() {
  let n = 1;
  while (true) {yield n++;}
}

// Before iterator helpers: convert to array first, but you can't,
// the generator is infinite. So you write a manual loop instead.
function firstFiveEvenSquares() {
  const result = [];
  for (const n of naturalNumbers()) {
    if (n % 2 === 0) {
      result.push(n * n);
      if (result.length === 5) {break;}
    }
  }
  return result;
}

That works, but it's imperative, and every new transformation means editing the loop body. Have you ever added a third condition to a loop like this and had to re-read the whole function to make sure you got the order right?

The Same Thing With Iterator Helpers

function* naturalNumbers() {
  let n = 1;
  while (true) {yield n++;}
}

const firstFiveEvenSquares = naturalNumbers()
  .filter(n => n % 2 === 0)
  .map(n => n * n)
  .take(5)
  .toArray();

// [4, 16, 36, 64, 100]

This reads in the order it executes: filter, then map, then take five, then materialize as an array. Nothing here converts the infinite generator to an array early, .take(5) stops pulling values the moment it has five, so .filter() and .map() only ever run against the first ten natural numbers, not an unbounded sequence.

Why Laziness Matters

Array.prototype.map() is eager: it runs the callback over every element and returns a full new array immediately. That's fine for a 50-item array. It's a problem for a generator reading lines from a multi-gigabyte log file, where you only want the first match.

function* readLines(source) {
  // imagine this yields one line at a time from a huge file
  for (const line of source) {yield line;}
}

const firstErrorLine = readLines(logSource)
  .filter(line => line.includes('ERROR'))
  .take(1)
  .toArray()[0];

With an array-based approach, you'd read the entire file into memory, filter it, then take the first result. With iterator helpers, reading stops the instant the first ERROR line is found. That's not a minor optimization, it's the difference between reading 10 lines and reading 10 million.

Which Methods Are Available

MethodWhat it doesEager or lazy
.map(fn)Transform each valueLazy
.filter(fn)Keep values matching a predicateLazy
.take(n)Stop after n valuesLazy
.drop(n)Skip the first n valuesLazy
.flatMap(fn)Map then flatten one levelLazy
.reduce(fn, init)Fold into a single valueEager, terminal
.toArray()Materialize as an arrayEager, terminal
.forEach(fn)Run a side effect per valueEager, terminal
.some(fn) / .every(fn)Boolean testsEager, terminal (short-circuits)

The pattern to remember: everything that returns another iterator is lazy and chainable. Everything that returns a concrete value, a number, a boolean, an array, is a terminal operation that actually pulls values through the whole chain.

Using Iterator Helpers on Built-in Iterables

They aren't limited to generator functions. Map.prototype.keys(), Map.prototype.values(), and Set.prototype.values() all return iterators, and they get the same helper methods:

const inventory = new Map([
  ['widget', 42],
  ['gadget', 0],
  ['gizmo', 17],
]);

const inStockNames = inventory
  .entries()
  .filter(([, count]) => count > 0)
  .map(([name]) => name)
  .toArray();

// ['widget', 'gizmo']

No Array.from(inventory.entries()) needed as a first step. The .entries() iterator itself now supports the full chain.

Writing Your Own Iterable That Benefits

Any class implementing Symbol.iterator gets these methods automatically, since the return value of Symbol.iterator is expected to be an iterator, and Iterator.prototype now sits in that chain:

class Range implements Iterable<number> {
  constructor(private start: number, private end: number) {}

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

const range = new Range(0, 1000000);
const firstTenSquares = range[Symbol.iterator]()
  .map(n => n * n)
  .take(10)
  .toArray();

A million-element Range costs nothing extra here beyond computing ten squares, because nothing before .take(10) runs eagerly.

Conclusion

Iterator helpers close a real gap: arrays had a fluent, chainable API for two decades, and every other iterable had to be converted to an array first to get the same ergonomics, at the cost of losing laziness and sometimes blowing up memory on large or infinite sequences. If you're writing a generator function in 2026 and reaching for Array.from() out of habit, check whether .filter(), .map(), or .take() on the iterator itself gets you there without the conversion.

Frequently Asked Questions

What are JavaScript iterator helpers?
Iterator helpers are a set of methods, map, filter, take, drop, flatMap, reduce, toArray, forEach, and a few more, added directly to the Iterator prototype. Before this, only arrays had these methods. Any object implementing the iterator protocol, a generator function, a Map's keys(), a custom class with Symbol.iterator, now gets them for free without you converting it to an array first.
Do I still need Array.from() with iterator helpers?
Only when you actually need an array at the end, for example to pass to a function that expects one, or to use array-only methods like sort(). If you're just chaining transformations and consuming the result with a for...of loop, you don't need Array.from() at all, iterator helpers let you skip the intermediate array entirely.
Are iterator helpers lazy?
Yes, and that's the main reason they exist. .map() on an iterator doesn't run the mapping function over every item immediately, it returns a new iterator that applies the mapping only as each value is pulled. Combined with .take(n), you can process an infinite generator and only ever compute the first n values, something Array.prototype.map() can't do because arrays are eager and finite by definition.