benjaminjohnson.me

Type definitions for async functions in TypeScript

04-22-2020 — 4 min read

One of the things that tripped me up when I first started using TypeScript was how to write type definitions for async functions.

Async/await syntax were added to ECMAScript in 2017 (or ES8), and it's incredibly useful for dealing with asynchronous code. If you're unfamiliar with async/await, this is what the syntax looks like:

async function fetchData(id) {
  const result = await fetch(`https://example.com/users/${id}`)
  const json = result.json()

  // json is an array of users
  return json
}

Or, if you prefer using arrow functions, the above function's equivalent would look like this:

const fetchData = async id => {
  const result = await fetch(`https://example.com/users/${id}`)
  const json = result.json()

  // json is an array of users
  return json
}

Changing a regular function into an async function happens just by using the async keyword in front of the function declaration. By doing this, we're able to use the await keyword to simply have our function pause execution until whatever we're awaiting is resolved.

Before we get to writing TypeScript definitions, it's important that we have a thorough understanding of how JavaScript promises work. The reason for this is that async/await is just a pretty syntax for promises! So when we get around to type definitions, typing an async function is exactly the same as typing a function that returns a promise.

Our sample function without promises would look like this:

function fetchData(id) {
  // resolves with an array of users
  return fetch(`https://example.com/users/${id}`).then(result => result.json())
}

This function and the async/await version are the same in terms of how they behave and what type of values they return. The difference is purely in the syntax.

💬 A quick note: since async/await is just a pretty syntax for promises, you might find that in some cases using the regular promise syntax feels cleaner. This is ok—it's not wrong to use async/await and promises interchangeably based on the situation. I personally think our example lends itself more to promises since there's only a single .then being chained.

Now to the type definitions!

Armed with the knowledge that an async function is a fancy syntax for a function that returns a promise, we can move on to writing the type definitions.

TypeScript has a built in Promise type we can use to describe promises—it's a generic type so we'll pass in the type that the promise resolves with.

interface User {
  id: string
  name: string
}

// This type describes a promise resolving with an array of user objects.
type UsersPromise = Promise<User[]>

Once we've identified how to wrap our User interface in the Promise type all that's left to do is attach it to our function and add the other type annotations:

interface User {
  id: string
  name: string
}

async function fetchData(id: string): Promise<User[]> {
  const result = await fetch(`https://example.com/users/${id}`)
  const json = result.json()

  // json is an array of users
  return json
}

We give id a type of string so that TypeScript knows how to infer its type, and then just attach the Promise<User[]> to fetchData as the return value. This tells TypeScript that the function operates asynchronously and anyone using it will need to either await it or use fetchData(id).then(users => {}) to actually make use of the fetched users data.

If you prefer to use type aliases for your functions, you can use the same TypeScript syntax along with declaring the function slightly differently to type the async function with a type alias:

interface User {
  id: string
  name: string
}

// Type alias
type FetchData = (id: string) => Promise<User[]>

const fetchData: FetchData = async id => {
  const result = await fetch(`https://example.com/users/${id}`)
  const json = result.json()

  // json is an array of users
  return json
}

This can be especially useful to decrease verbosity if the async function you're typing has multiple or complex parameters.


Thanks for reading—I hope that this was helpful to you in figuring out how to still use async/await syntax while giving TypeScript enough info to properly infer the shapes of the data your async functions are returning.

As always, feel free to hit me up on my Twitter or LinkedIn with any thoughts. If you found a typo in this article or have some feedback for improvement please feel free to sumbit a pull request.

Cover photo by Photo Boards on Unsplash