async/await and Typing Promises

Intermediate
11 min

async/await and Typing Promises

Almost every TypeScript application talks to a network, a database or the file system, so most functions you write will be asynchronous. TypeScript types promises precisely: an async function always returns Promise<T>, await unwraps it, and combinators such as Promise.all preserve the type of each input. This lesson covers those rules, the Awaited utility, how to type data that arrives from outside, and the mistakes that let promises slip through unhandled.

Promise<T> and async Functions

Promise<T> is a generic type whose parameter is the resolved value. Declaring a function async wraps its return value automatically:

typescript
async function loadConfig(): Promise<{ port: number }> { return { port: 3000 }; // wrapped in a Promise for you } async function nothing(): Promise<void> { /* no return */ } const p = loadConfig(); // Promise<{ port: number }> const cfg = await loadConfig(); // { port: number }

The annotation Promise<...> is optional because it is inferred, but adding it to exported functions documents the contract and catches a branch that forgets to return. Annotating an async function with a non-Promise return type is an error.

await is only allowed inside async functions and at the top level of ES modules (requires module set to es2022, esnext, nodenext or preserve).

Creating Promises Manually

Wrapping callback-style APIs still happens. Give the constructor its type argument so resolve is checked:

typescript
function delay(ms: number): Promise<void> { return new Promise((resolve) => setTimeout(resolve, ms)); } function readFileAsync(path: string): Promise<string> { return new Promise<string>((resolve, reject) => { fs.readFile(path, "utf8", (err, data) => (err ? reject(err) : resolve(data))); }); }

Node.js already exposes promise versions of most APIs (import { readFile } from "node:fs/promises") and util.promisify converts the rest, so hand-written wrappers should be rare.

Combinators Keep Their Types

| Combinator | Result type | Rejects when | |---|---|---| | Promise.all([a, b]) | [A, B] tuple | Any input rejects | | Promise.allSettled([a, b]) | PromiseSettledResult<A \| B>[] | Never | | Promise.race([a, b]) | A \| B | The first settled promise rejects | | Promise.any([a, b]) | A \| B | All inputs reject (AggregateError) |

Promise.all on an array literal infers a tuple, so destructuring gives each element its own type. PromiseSettledResult is a discriminated union on status, which is why the if (r.status === "fulfilled") check in the sample narrows to r.value.

The Awaited Utility Type

Awaited<T> computes what await would produce, recursively unwrapping nested promises. It is what you need when deriving a type from an async function:

typescript
async function getOrders() { return [{ id: 1, total: 42 }]; } type OrdersPromise = ReturnType<typeof getOrders>; // Promise<{ id: number; total: number }[]> type Orders = Awaited<ReturnType<typeof getOrders>>; // { id: number; total: number }[]

Typing Data From the Outside

response.json() returns Promise<any>, and any spreads silently. Always assign the result to unknown (or a typed schema parser) and narrow it before use, as fetchUser does in the sample. A generic wrapper keeps call sites clean while forcing validation:

typescript
async function getJson<T>(url: string, guard: (v: unknown) => v is T): Promise<T> { const res = await fetch(url); if (!res.ok) throw new Error(`HTTP ${res.status} for ${url}`); const data: unknown = await res.json(); if (!guard(data)) throw new Error(`Unexpected payload from ${url}`); return data; }

Inside a catch, the error variable is unknown under strict; check err instanceof Error before reading .message.

Async Iteration

Streams, paginated APIs and readline interfaces expose AsyncIterable<T>. for await consumes them with full typing:

typescript
async function* pages(): AsyncGenerator<string[], void, undefined> { let cursor: string | undefined; do { const page = await getJson(`/api/items?cursor=${cursor ?? ""}`, isPage); cursor = page.next; yield page.items; } while (cursor); } for await (const items of pages()) console.log(items.length);

Common mistakes

  • Forgetting await and passing a Promise<User> where a User is expected. The error message names the Promise type; read it.
  • Fire-and-forget calls such as saveLog(entry); inside an async function. Enable the @typescript-eslint/no-floating-promises rule or prefix intentional ones with void.
  • await inside forEach callbacks, which does not wait; use for...of or Promise.all(array.map(...)).
Quick Quiz
Question 1 of 3

What is the return type of `async function f() { return 42; }`?

Key Takeaways

  • async functions always return Promise<T>; await unwraps it, and annotating the Promise return type documents the API.
  • Promise.all infers a tuple of results; Promise.allSettled yields a discriminated union on status.
  • Awaited<ReturnType<typeof fn>> derives the resolved type of an async function.
  • Treat response.json() as unknown and validate with a guard or schema before trusting it.
  • Never leave promises floating: await them, return them, or mark them with void deliberately.

Next lesson: Error Handling Patterns — typed errors, custom error classes, result types and assertion helpers.

async/await and Typing Promises - TypeScript | CodeYourCraft | CodeYourCraft