A new blog post just dropped 📣 Click here to read it.

← Back to Writings

Generators In JavaScript

📅 September 27, 2021 • ⌛ 4 min read🏷 typescript🏷 react🏷 javascript✏️ Edit this post on GitHub

Functions are general concepts in a programming language that helps to group thought and return a single result. In this article, I will show you how to write your generator functions.

You will explore how to create generator functions, get familiar with its methods and application programming interface - API (throw(), return(), yield). Get a better understanding of iterators and AsyncIterators, understand how to types both iterators and generators for TypeScript usage, and explore possible use cases for them.

At the end of this article, you will be able to differentiate between the functions and generator functions. Be comfortable using and explaining them to interviewers and co-workers when asked about them.

Prerequisites

The requirement to follow this tutorial is the basic understanding of functions, arrays and loops. Good knowledge of await/async, promises and asynchronous javascript will be helpful. The target is javascript developers who have not used the generator function at all. For typescript developers to learn how to type generators, check the section for typescript developers.

Examples

function myName() {
  // i can return a string, number, array, object or promise that resolve to all the listed data types
  return 'ojo'
}
myName()
function* myNameAgeGithubUsername() {
  yield 'ojo'
  yield 28
  yield 'Oluwasetemi'
}
let generator = myNameAgeGithubUsername()

console.log(generator) // {[ Generator]}

console.log(generator.toString()) // [object Generator]

let name = generator.next()
console.log(name)
let age = generator.next()
console.log(age)
let githubUsername = generator.next()
console.log(githubUsername)

// you can use a for of loop on this generator
for (let each of myNameAgeGithubUsername()) {
  console.log(each) // ojo, 28
}
// you can spread the value like an array
let arrayLike = [...myNameAgeGithubUsername()]
console.log(arrayLike)

Explaining the Keywords

Iteration is repeating a set of instructions over a period of a condition, also referred to as loops depending on the context of usage. Iterators help with sequence, that is, returning of consistent value one after another. If I say a string value is iterable, it means it can be loopped/iterated.

let name = 'generator'

for (let each = 0; each < name.length; each += 1) {
  // looping through the string;
  console.log(name[each]) // prints each letter in the string
}

Any object that implements the Iterator protocol can be looped. The iterator protocol is an object that has a next method. The next method returns an object containing value - the next value and done - a boolean value that denotes if the iteration is complete or not. Once the iterator is created, it can be looped explicitly by calling next() and the for of loop. When there is no more value to yield, it returns {done: true}.

Generators can return (“yield”) multiple values, one after another, on-demand. They work great with iterable, allowing to create data streams with ease.

Now that you have a clear understanding of all the major concepts you will be exploring in this tutorial, let’s get it rolling.

Built-in Iterables and Statements

String, Array, TypedArray, Map and Set are all built-in iterable because their prototype objects all have a Symbol.iterator method.

Some statements and expressions expected are for-of loops, yield*, yield and function*.

Iterator Example

function makeEvenRangeIterator(start = 2, end = Infinity, step = 2) {
  let nextIndex = start
  let iterationCount = 0

  const rangeIterator = {
    next: function () {
      let result
      if (nextIndex < end) {
        result = {value: nextIndex, done: false}
        nextIndex += step
        iterationCount++
        return result
      }
      return {value: undefined, done: true, count: iterationCount}
    },
  }
  return rangeIterator
}

let it = makeEvenRangeIterator(2, 10)
let result = it.next()
while (!result.done) {
  console.log(result.value) // 2, 4, 6, 8, 10
  result = it.next()
}
// adapted from [MDN](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Iterators_and_Generators)

Note - The real thing working behind the scene in iterators is theSymbol.iterator. You can go further by implementing the previous example using the Iterator protocol.

const evenRange = {
  from: 2,
  to: 10,
}

evenRange[Symbol.iterator] = function () {
  console.log(this.from)
  return {
    current: this.from,
    last: this.to,
    count: 0,

    next() {
      if (this.current < this.last) {
        this.count++
        return {done: false, value: (this.current += 2)}
      } else {
        return {done: true, count: this.count}
      }
    },
  }
}

// you can loop with the traditional for of loop
for (let i of evenRange) {
  console.log(i)
}

You should have a solid understanding of the iterators and their protocol, you should be able to create an iterator.

Creating Generators

Generators is used to yield value as opposed to the traditional function. It takes iteration to another level of returning the value one at a time using a function. There exist also async generator - which will be discussed later in this tutorial.

You will look at async generator and an example but let’s create a generator example. Symbol.iterator will help you create generators as also in the case of iterators but the only difference is the * in the function which enables it to yield its values(it adds the generating effect).

In the example, you will create a range between two numbers with the value 1 as the step in between each values.

let rangeGenerator = {
  from: 1,
  to: 5,

  *[Symbol.iterator]() {
    // a shorthand for [Symbol.iterator]: function*()
    for (let value = this.from; value <= this.to; value++) {
      yield value
    }
  },
}

// this can be used in any for of loop, arrayLike, like an iterator.
// Please pause and write how this can be used as
// 1.array with array spread operator.
// 2.like iterator using `.next()`
// 3.consumed with a for of loop

You can drop your answers in the comment section.

Yielding means you should be expecting an answer to a question, it requires you to wait till the answer is returned. You can compose generators using the yield* syntax. This means returning a generator within a generator. Generator composition is a special feature of generators that allows transparent “embeding” of generators in each other.

function* generateSequenceCompose(start, end) {
  for (let i = start; i <= end; i++) yield i
}

function* generatePasswordCodes() {
  // 0..9
  // for (let i = 48; i <= 57; i++) yield i;
  yield* generateSequenceCompose(48, 57)

  // A..Z
  // for (let i = 65; i <= 90; i++) yield i;
  yield* generateSequenceCompose(65, 90)

  // a..z
  // for (let i = 97; i <= 122; i++) yield i;
  yield* generateSequenceCompose(97, 122)
}

// used
let str = ''

for (let code of generatePasswordCodes()) {
  str += String.fromCharCode(code)
}
console.log(str) // 0..9A..Za..z

// copied from https://javascript.info/generators

generator.throw

You can force a generator to throw an error in the place where you intended a yield. It is passed an error object, let’s consider an example:

function* myGen() {
  yield 1
}
myGen().throw(new Error('throw error using the generator.throw'))

Use cases for using .throw might be limited but it is a good tool to have if you like to return an error when the network is out instead of allowing the API to make a request. If you know any possible use case for this method, kindly share using the comment section.

generator.return(value)

This is used to finish the generator execution before its time and return the value passed to the generator.return Let consider am example:

function* myGen() {
  yield 1
  yield 2
}
let gen = myGen()
console.log(gen.next()) // return {done: false: value: 1}
console.log(gen.return('I am done')) // {done: true, value: 'I am done'}
gen.next() // {done: true, value: undefined}

I cannot think of any use cases for this method, please share any in the comment section.

Async Generators

They are used to read the stream(constant inflow) of data. You can hit a paginated endpoint and fetch all the data using an async generator at a go. They can be used with the for await of loop just like generators can be used with the for of loop. Before showing an example of Async generators, Recall that iterators are created using Iterator protocol (@@iterator) - Symbol.iterator. Async generators are created using Symbol.asynciterator or by adding an async to the generator function to make it an async generator function.

async function* asyncGeneratorOneTwo() {
  yield new Promise(resolve => resolve(1))
  yield new Promise(resolve => resolve(2))
}

let asyncGen = asyncGeneratorOneTwo()

;(async () => {
  for await (let each of asyncGen) {
    console.log(each) // 1,2
  }
})()

TypeScript Example

Please take note of the types for typescript users. AsyncIterableIterator<T> and IterableIterator<T>.

The example creates a delay promise using the setTimeout, getRandomSetChars function returns random string characters not more than 5 characters that can be called in the getRandomSetChars async generator that yields 3 values. It yields a random string, then yields a delay of 200ms and finally an array of two random strings.

function delay(ms: number): Promise<void> {
  return new Promise<void>(resolve => {
    setTimeout(resolve, ms)
  })
}
function getRandomSetChars(): string {
  const random = 1 + Math.floor(Math.random() * 5)
  let wordString = ''
  for (let i = 0; i < random; i++) {
    const letter = 97 + Math.floor(Math.random() * 26)
    wordString += String.fromCharCode(letter)
  }
  return wordString
}
// Step 3
async function* getRandomSetsChars(): AsyncIterableIterator<string> {
  for (let i = 0; i < 10; i++) {
    yield getRandomSetChars() // return a random set of char
    await delay(200) // wait
    yield* [getRandomSetChars(), getRandomSetChars()] // return two random sets of char
  }
}
// Step 4
async function addWordsAsynchronously() {
  for await (const x of getRandomSetsChars()) {
    console.log('Iterator loop:' + x)
  }
}
addWordsAsynchronously()

Real World Use Case With GitHub API

They are used in Rxjs and Redux-Saga. This use case allows a user to fetch all the commits on a GitHub repository using GitHub API iteratively until the commits get to 100 while logging to the console the name of the commit author.

The beauty is fetching from an endpoint in a repeated manner until you have fetched the required 100 commits. It helps fetch all the data you will need to load the next page using a feature that GitHub implements in all its paginated endpoints where it returns the next page URL of a paginated resource in the Link field of the response.headers. This allows hitting the next endpoint asynchronously after fetching the 30 commits returned by Github API for the paginated resource. The example is adapted from javascript.info.

async function* fetchCommits(repo) {
  let url = `https://api.github.com/repos/${repo}/commits`

  while (url) {
    const response = await fetch(url, {
      // (1)
      headers: {'User-Agent': 'Our script'}, // github requires user-agent header
    })

    const body = await response.json() // (2) response is JSON (array of commits)

    // (3) the URL of the next page is in the headers, extract it
    let nextPage = response.headers.get('Link').match(/<(.*?)>; rel="next"/)
    nextPage = nextPage && nextPage[1]

    url = nextPage

    for (let commit of body) {
      // (4) yield commits one by one, until the page ends
      yield commit
    }
  }
}

;(async () => {
  let count = 0

  for await (const commit of fetchCommits(
    'javascript-tutorial/en.javascript.info',
  )) {
    console.log(commit.author.login)

    if (++count == 100) {
      // let's stop at 100 commits
      break
    }
  }
})()

Conclusion

In my experience, there are not many use cases for generators in the common day-to-day task, but exploring them shows us the possibilities within the JavaScript language. The ability to return data yielding it one at a time is awesome, more fascinating is the ability to await a yield value.

Attempt Quiz

What is the difference between the two code snippets below? You can drop your answers in the comment section.

function* myNameAgeGithubUsernameA() {
  yield 'ojo'
  yield 28
  yield 'Oluwasetemi'
}

function* myNameAgeGithubUsernameB() {
  yield 'ojo'
  yield 28
  return 'Oluwasetemi'
}

Credit


Comments Should Load Here 😜

Loading script...
Ojo Oluwasetemi

Written by Oluwasetemi Ojo Stephen {...OOS}, A FullStack Developer (Reactjs, Nodejs, Typescript), currently lives in Osogbo, Osun State Nigeria with my lovely and priceless Wife Temidayo .🎈
Say Hi to Him on Twitter.
You can search through my blog using custom tags• 🏷 .
Click here to read more about me. For RSS feed.🌍