Async, Iterators and Generators

This section documents the current TypeScript release line as published at the official TypeScript documentation, which is the reference these pages are written and verified against. No specific patch version is pinned.

This content was generated with the assistance of AI and should be verified against the official documentation before being relied on in production, since TypeScript iterates quickly.

This section’s bibliography lists the reference material consulted while preparing these pages.

The runtime semantics are exactly JavaScript’s — see Asynchronous JavaScript and Iterators and Generators. This page is about the types: the generic wrappers (Promise<T>, Generator<T, TReturn, TNext>), how await and for…​of propagate element types, and the target / lib settings each feature needs. The official Iterators and Generators handbook chapter is the companion reference.

Promise<T>

Promise<T> is the generic that types an eventual value: T is what the promise resolves to. A rejection carries no type — it surfaces as any (or unknown with useUnknownInCatchVariables) at the catch. See Generics for how T threads through .then.

function fetchUser(id: number): Promise<{ id: number; name: string }> {
  return fetch(`/api/users/${id}`).then((res) => res.json());
}

fetchUser(1).then((user) => {
  user.name; // string -- T flows through .then
});

Typing new Promise

The executor’s resolve is untyped unless you supply the type argument, so always write new Promise<T>. Without it TypeScript infers Promise<unknown>.

const ready = new Promise<string>((resolve, reject) => {
  setTimeout(() => resolve("done"), 100); // resolve now requires a string
  // resolve(42); // Error: number is not assignable to string
});

async functions always return a promise

An async function is always Promise-returning. You annotate the inner type and TypeScript wraps it; a bare value in a return is fine. The return annotation itself must be a Promise<…​> (or any).

async function load(): Promise<number> {
  return 42; // wrapped -> Promise<number>
}
// async function bad(): number {}  // Error: an async function's return type must be Promise<T>

await and Awaited<T>

await unwraps a Promise<T> to T, recursively (a promise of a promise collapses). Awaited<T> is the utility type that models this and is what the compiler uses internally.

const value: number = await load(); // Awaited<Promise<number>> = number

type A = Awaited<Promise<string>>;           // string
type B = Awaited<Promise<Promise<boolean>>>; // boolean -- unwraps recursively
type C = Awaited<number>;                     // number -- non-promises pass through

See Awaited<Type> in the utility types handbook, and Utility Types.

Promise.all / Promise.allSettled tuple typing

Passed an array literal, both methods infer a tuple type and map each element through Awaited<T>, so a destructuring assignment keeps every position’s precise type.

const [user, count, label] = await Promise.all([
  fetchUser(1),        // Promise<{ id: number; name: string }>
  Promise.resolve(42), // Promise<number>
  "literal" as const,  // not a promise -- passed through unchanged
]);
// user: { id: number; name: string }, count: number, label: "literal"

const settled = await Promise.allSettled([fetchUser(1), fetchUser(2)]);
// settled: [PromiseSettledResult<User>, PromiseSettledResult<User>]
for (const r of settled) {
  if (r.status === "fulfilled") {
    r.value; // User -- narrowed by the discriminant
  } else {
    r.reason; // any
  }
}

PromiseSettledResult<T> is a discriminated union on status. Promise.allSettled needs lib to include ES2020.Promise (automatic at target ES2020+); Promise.any and AggregateError need ES2021.

Top-level await

At module scope you can await without an async wrapper. It requires module set to es2022, esnext, system, node16, node18, or nodenext, and target es2017 or later.

// config.ts -- a module, not inside any function
const res = await fetch("/config.json");
export const config: { port: number } = await res.json();

async over callbacks, to improve type flow

A Node-style callback splits the result across parameter positions ((err, data) ⇒ …​) and the type is lost at every boundary. A Promise carries T through await, .then, and Promise.all, so wrapping a callback API once at the edge keeps the rest of the code inside the checked async data flow.

import { readFile } from "node:fs";

// Callback-to-promise wrapper: annotate the resolve type, reject the error.
function readFileAsync(path: string): Promise<Buffer> {
  return new Promise<Buffer>((resolve, reject) => {
    readFile(path, (err, data) => {
      if (err) reject(err);
      else resolve(data);
    });
  });
}

const text = (await readFileAsync("./notes.txt")).toString("utf8"); // Buffer -> string

Node’s util.promisify does this generically, with typed overloads that recover Promise<T> from the callback’s data parameter — prefer it when the API follows the (err, value) convention.

Iterators and iterables

Iterable<T> has a [Symbol.iterator]() method returning an Iterator<T>; Iterator<T, TReturn, TNext> has next() returning IteratorResult<T> (a union of \{ value: T; done: false } and \{ value: TReturn; done: true }). IterableIterator<T> is both at once — an iterator that returns itself from [Symbol.iterator] — which is what generators and the built-in collection iterators produce.

const range: Iterable<number> = {
  [Symbol.iterator](): Iterator<number> {
    let i = 0;
    return {
      next(): IteratorResult<number> {
        return i < 3 ? { value: i++, done: false } : { value: undefined, done: true };
      },
    };
  },
};

for (const n of range) {
  n; // number
}

for…​of typing

The loop variable takes the element type of the Iterable<T> being consumed: T[] yields T, a Map<K, V> yields [K, V], a Set<T> yields T, and a string yields string (one code point at a time). Iterating a plain object is still a type error — objects are not iterable.

const scores = new Map<string, number>([["ada", 10]]);
for (const [name, score] of scores) {
  name;  // string
  score; // number
}

target, downlevelIteration, and lib

  • lib must include the iterable declarations (ES2015.Iterable) for Symbol.iterator to be typed — automatic when target is es2015/es6 or later.

  • When target is es5/es3, for…​of, spread, and array destructuring over an arbitrary iterable (a Map, Set, or generator) only down-compile correctly with "downlevelIteration": true, which emits helper code that calls Symbol.iterator. Without it, only array/string iteration is lowered (by index).

  • At target es2015+ native iteration is emitted and downlevelIteration is unnecessary.

{ "compilerOptions": { "target": "es5", "downlevelIteration": true,
  "lib": ["es5", "es2015.iterable", "dom"] } }

Generator functions

A function* returns a Generator<T, TReturn, TNext>: T is each yield`ed value, `TReturn the final return value, and TNext the type a yield expression evaluates to (what the caller passes to next(x)). The return annotation must be Generator<…​> or IterableIterator<…​>. Because a generator object is an IterableIterator<T>, it drops straight into for…​of and spread — where the TReturn value is not visited.

function* countUp(limit: number): Generator<number, string, void> {
  let i = 0;
  while (i < limit) {
    yield i++;
  }
  return "done"; // TReturn -- seen by g.next(), not by for...of
}

const g = countUp(3);
g.next(); // { value: 0, done: false }
for (const n of countUp(3)) {
  n; // number
}

TNext types the two-way channel: the value handed to next() becomes the result of the paused yield.

function* accumulator(): Generator<number, void, number> {
  let total = 0;
  while (true) {
    const add: number = yield total; // supplied by next(add)
    total += add;
  }
}

const acc = accumulator();
acc.next();   // { value: 0, done: false } -- first next() has no yield to feed
acc.next(10); // { value: 10, done: false }
acc.next(5);  // { value: 15, done: false }

Async iterators

AsyncIterable<T> has [Symbol.asyncIterator]() returning an AsyncIterator<T> whose next() returns Promise<IteratorResult<T>>. for await…​of consumes any AsyncIterable<T> (and also a sync iterable of promises), awaiting each step; the loop variable is T. An async function* returns an AsyncGenerator<T, TReturn, TNext>, which is an AsyncIterableIterator<T>.

async function* streamPages(url: string): AsyncGenerator<string[], void, void> {
  let next: string | null = url;
  while (next) {
    const res = await fetch(next);
    const page: { items: string[]; next: string | null } = await res.json();
    yield page.items; // T = string[]
    next = page.next;
  }
}

for await (const items of streamPages("/api/logs")) {
  items; // string[]
}

Requires lib to include ES2018.AsyncIterable (automatic at target es2018+); for await at module top level needs the same module / target settings as top-level await.

using / await using and explicit resource management

TypeScript 5.2 implemented the TC39 explicit resource management proposal. using x = …​ binds a resource whose [Symbol.dispose]() runs when the enclosing block exits — including on throw — like a scoped try/finally. await using x = …​ instead awaits [Symbol.asyncDispose](). The initializer must be Disposable (or AsyncDisposable for await using); DisposableStack / AsyncDisposableStack aggregate several resources.

function openHandle(path: string): Disposable & { read(): string } {
  // ...open the file...
  return {
    read: () => "contents",
    [Symbol.dispose]() { /* close the file */ },
  };
}

{
  using h = openHandle("./data");
  h.read();
} // h[Symbol.dispose]() called here, even if read() threw

class Pool implements AsyncDisposable {
  async [Symbol.asyncDispose](): Promise<void> { /* drain connections */ }
}

async function run(): Promise<void> {
  await using pool = new Pool();
} // await pool[Symbol.asyncDispose]() on exit

lib / target requirements: the Disposable, AsyncDisposable, and Symbol.dispose / Symbol.asyncDispose declarations come from the esnext.disposable lib. Add it explicitly unless target already pulls it in ("lib": ["es2022", "esnext.disposable", "dom"], or just "lib": ["esnext"]). When down-levelling, TypeScript emits a Symbol.for-based fallback for the disposal symbols, but the runtime still needs Symbol support. See the TypeScript 5.2 release notes.

See also

  • Asynchronous JavaScript — promises, async/await, and event-loop semantics at runtime.

  • Iterators and Generators — the iteration protocols and function* in plain JavaScript.

  • Generics — how Promise<T> and the iterator types propagate their type parameters.

  • Utility Types — Awaited<T> and the other standard type-level helpers.