Skip to content

Iteration protocols in JavaScript

Iterations protocols allow us to loop over elements using a common interface. There are three main constructs that can be confusing sometimes:

Iterators

Iterators are any objects that implement the next method with the following semantics:

type Iterator<T> = {
  next(): {
    done: boolean;
    value: T;
  };
};
function idMaker(maxId) {
  let index = 0;
  return {
    next() {
      if (index > maxId) {
        return { done: true };
      }
      return {
        value: index++,
        done: false,
      };
    },
  };
}

To use an iterator:

const iterator = idMaker(10);
let result = iterator.next();
while (!result.done) {
  console.log(result.value); // 0, 1, 2, ..
  result = iterator.next();
}

Iterables

Iterables are objects that return an iterator (what was defined above).

type Iterable<T> = {
  [Symbol.iterator](): Iterator<T>;
};

They are more general than iterators, in the following example IdMaker is a class with extra methods:

class IdMaker {
  constructor(maxId) {
    this.maxId = maxId;
  }

  label(id) {
    return `ID: ${id}`;
  }

  // implementing the iterator() function that returns an iterator
  [Symbol.iterator]() {
    let index = 0;
    const maxId = this.maxId;
    return {
      next() {
        if (index > maxId) {
          return { done: true };
        }
        return {
          value: index++,
          done: false,
        };
      },
    };
  }
}

We can use more looping constructs with iterables, for example the for/of, and the spread syntax:

const idMaker = new IdMaker(10);
for (const id of idMaker) {
  console.log(id);
}
const idMaker = new IdMaker(10);
console.log([...idMaker]);

We can also access the iterator of an iterable directly to loop over results individually:

const idMaker = new IdMaker(10);
const iterator = idMaker[Symbol.iterator](); // we access the iterator first
let result = iterator.next();
while (!result.done) {
  console.log(result.value); // 0, 1, 2, ..
  result = iterator.next();
}

Note: Iterators can also become iterables by implementing the iterator() method:

function idMaker(maxId) {
  let index = 0;
  return {
    next() {
      if (index > maxId) {
        return { done: true };
      }
      return {
        value: index++,
        done: false,
      };
    },
    // here:
    [Symbol.iterator]() {
      return this;
    },
  };
}

Generators

Generators can also be used instead of iterators for a simpler syntax:

class IdMaker {
  constructor(maxId) {
    this.maxId = maxId;
  }

  label(id) {
    return `ID: ${id}`;
  }

  // the iterator is a generator
  *[Symbol.iterator]() {
    for (let index = 0; index < this.maxId; index++) {
      yield index;
    }
  }
}

To loop over the results, we can use the same constructs for iterables like for/of and spread syntax.