CodeFixesHub
    programming tutorial

    Typing Async Iterators and Async Iterables in TypeScript — Practical Guide

    Learn to type async iterators and iterables in TypeScript with hands-on examples, error handling, and best practices. Follow this step-by-step tutorial.

    article details

    Quick Overview

    TypeScript
    Category
    Sep 9
    Published
    23
    Min Read
    2K
    Words
    article summary

    Learn to type async iterators and iterables in TypeScript with hands-on examples, error handling, and best practices. Follow this step-by-step tutorial.

    Typing Async Iterators and Async Iterables in TypeScript — Practical Guide

    Introduction

    Async iterators and async iterables unlock powerful streaming and incremental-processing patterns in modern JavaScript and TypeScript—enabling you to consume data as it becomes available without blocking the event loop. For intermediate developers, understanding how to type these constructs precisely in TypeScript helps eliminate runtime surprises, improves DX (developer experience), and enables safer refactors across codebases that rely on streaming APIs, databases, or event sources.

    This guide teaches you practical, real-world patterns for typing async iterators and async iterables in TypeScript. You will learn how to: define typed async generators and custom async iterable objects, type the values yielded and returned, combine async iterators with generics and mapped types, handle errors safely, and integrate these patterns with existing APIs (e.g., streaming JSON, Node.js streams). Throughout the article you'll find code examples, step-by-step explanations, performance tips, and troubleshooting guidance.

    By the end, you will be comfortable writing clear type signatures for async iterators, using them in public APIs, and applying advanced techniques like narrowing yields with type guards and shaping returned types with the satisfies operator. We also connect typing patterns to related TypeScript topics so you can deepen your knowledge (e.g., using const assertions, type guards, and typing promise rejections).

    Background & Context

    An async iterable is an object with a Symbol.asyncIterator method that returns an async iterator. An async iterator is an object with an async next() method that returns a Promise resolving to an IteratorResult<T, TReturn>. Async iterators power the "for await...of" syntax, streaming deserialization, lazy database cursors, and more.

    TypeScript defines built-in types like AsyncIterable, AsyncIterator<T, TReturn = any, TNext = undefined>, and AsyncGenerator<T, TReturn, TNext>. However, real-world uses require combining these with generics, discriminated unions, and error types for precise APIs. Precise typing prevents accidental misuse (e.g., assuming next() resolves synchronously or that yield values can be null) and guides consumers.

    For readers who need a broader foundation on related typing techniques—like typing asynchronous generator functions in depth—see our complementary guide on Typing Asynchronous Generator Functions and Iterators in TypeScript.

    Key Takeaways

    • How to type async generators and custom async iterable objects.
    • How to type yielded values (T), returned values (TReturn), and next values (TNext).
    • Using generics and utility types to create reusable async-iterator APIs.
    • Common pitfalls: mismatched IteratorResult types, forgetting Promise wrappers, and incorrect inference.
    • Advanced patterns: type narrowing with guards, typing errors, and interop with Node.js streams.

    Prerequisites & Setup

    You should be familiar with basic TypeScript types, generics, and the standard synchronous iterator protocol (Iterable/Iterator). Node.js 12+ or a modern browser (for "for await...of") is required to run examples. Use TypeScript 4.x or newer for the best type inference and features like improved inference on generators and the satisfies operator.

    Suggested project setup:

    • Node.js 16+ (recommended) or a modern browser.
    • TypeScript 4.9+ for nicer features—install via npm: npm install --save-dev typescript.
    • tsconfig.json with target ES2018+ and lib including ES2018 and DOM if needed so AsyncIterable is available.

    For guidance on typing streaming payloads and validating external JSON while streaming, consult our article on Typing JSON Payloads from External APIs (Best Practices).

    Main Tutorial Sections

    1. Async Generator Basics: Typing a Simple async function*

    A common pattern is an async generator function that yields values over time. The TypeScript type for such a function is inferred, but you can explicitly annotate it using AsyncGenerator:

    ts
    async function* numbers(): AsyncGenerator<number, void, unknown> {
      for (let i = 0; i < 3; i++) {
        await new Promise((r) => setTimeout(r, 100));
        yield i; // yield type: number
      }
    }
    
    async function demo() {
      for await (const n of numbers()) {
        console.log(n); // n is typed as number
      }
    }

    Here AsyncGenerator<number, void, unknown> means: yield numbers (T = number), the generator returns void (TReturn = void), and it accepts unknown values via next() (TNext = unknown). Explicit annotations help when inference is insufficient or when publishing library types.

    For more patterns around async generators and inference, see Typing Asynchronous Generator Functions and Iterators in TypeScript.

    2. Implementing a Custom AsyncIterable Object

    You can implement an object conforming to AsyncIterable. Doing so often allows more control than an async generator:

    ts
    interface AsyncIterableSource<T> extends AsyncIterable<T> {
      close?: () => Promise<void>;
    }
    
    function makeCounter(max: number): AsyncIterableSource<number> {
      let i = 0;
      return {
        async *[Symbol.asyncIterator]() {
          while (i < max) {
            await new Promise((r) => setTimeout(r, 50));
            yield i++;
          }
        }
      };
    }
    
    const it = makeCounter(3);

    Typing the object as AsyncIterableSource makes the public contract explicit. If you need to expose close(), include it in your interface.

    3. Typing IteratorResult and next(): Distinguish Yield vs Return

    IteratorResult<T, TReturn> is a union: { value: T; done: false } | { value: TReturn; done: true }. When implementing next() manually, return properly formed results:

    ts
    type Result<T, R> = Promise<IteratorResult<T, R>>;
    
    class AsyncRange implements AsyncIterator<number, void> {
      constructor(private i = 0, private end = 3) {}
      async next(): Result<number, void> {
        if (this.i < this.end) {
          return { value: this.i++, done: false };
        }
        return { value: undefined as any, done: true }; // careful: types
      }
    }

    Note: returning "value: undefined as any" is awkward and error-prone. Prefer annotating TReturn properly or using void/undefined consistently to avoid casts.

    If you're building recursive or tree-like streams, check typing strategies in Typing Recursive Data Structures in TypeScript.

    4. Generics: Building Reusable AsyncIterator Factories

    Generics let you create typed factories for async iterators, e.g., streaming paginated API results:

    ts
    type Page<T> = { items: T[]; nextCursor?: string };
    
    async function* paginate<T>(fetchPage: (cursor?: string) => Promise<Page<T>>): AsyncGenerator<T, void, unknown> {
      let cursor: string | undefined = undefined;
      while (true) {
        const page = await fetchPage(cursor);
        for (const item of page.items) yield item;
        if (!page.nextCursor) break;
        cursor = page.nextCursor;
      }
    }

    This signature allows consumers to infer item types automatically. Combine this with strict JSON typing strategies when dealing with external APIs—see Typing JSON Payloads from External APIs (Best Practices).

    5. Typing Errors: Async Iterators That Can Reject

    Async iterators yield Promises (via next()), so they can reject. To make errors explicit, typeguarding and documentation are key because TypeScript does not track thrown exceptions on Promise types directly.

    Best practice: define explicit error types and runtime guards. For example:

    ts
    class APIError extends Error { constructor(public code: number, msg: string) { super(msg); }}
    
    async function* streamWithErrors(): AsyncGenerator<number, void, unknown> {
      for (let i = 0; i < 5; i++) {
        if (i === 3) throw new APIError(500, 'Server error');
        yield i;
      }
    }
    
    async function consume() {
      try {
        for await (const n of streamWithErrors()) console.log(n);
      } catch (err) {
        if (err instanceof APIError) {
          // handle typed error
        }
      }
    }

    For patterns on typing promises and their rejections, see Typing Promises That Reject with Specific Error Types in TypeScript and for error object types, see Typing Error Objects in TypeScript: Custom and Built-in Errors.

    6. Interop with Node.js Streams and Async Iterators

    Node.js streams are common sources of data that you may want to consume as async iterables (or expose as such). Node 10+ supports async iteration over readable streams via readableSymbol.asyncIterator. Use type wrappers to ensure consistent item typing:

    ts
    import { createReadStream } from 'fs';
    
    // If reading JSON lines, you might parse each chunk to a typed record
    async function* streamJsonLines<T>(path: string): AsyncGenerator<T, void, void> {
      const rs = createReadStream(path, { encoding: 'utf8' });
      // Node Readable implements AsyncIterable<Buffer|string>
      let buffer = '';
      for await (const chunk of rs as AsyncIterable<string>) {
        buffer += chunk;
        let idx: number;
        while ((idx = buffer.indexOf('
    ')) >= 0) {
          const line = buffer.slice(0, idx);
          buffer = buffer.slice(idx + 1);
          yield JSON.parse(line) as T;
        }
      }
    }

    See patterns for typing Node.js built-ins in Typing Node.js Built-in Modules in TypeScript when integrating streams with TypeScript.

    7. Type Guards and Narrowing Yielded Values

    If your async iterator yields heterogeneous values (e.g., events), use discriminated unions and type guards to narrow the types at consumption time:

    ts
    type Event = { kind: 'data'; payload: string } | { kind: 'end' } | { kind: 'error'; error: Error };
    
    async function* eventSource(): AsyncGenerator<Event, void, unknown> {
      yield { kind: 'data', payload: 'a' };
      yield { kind: 'error', error: new Error('boom') };
      yield { kind: 'end' };
    }
    
    function isData(e: Event): e is { kind: 'data'; payload: string } {
      return e.kind === 'data';
    }
    
    async function consume() {
      for await (const e of eventSource()) {
        if (isData(e)) {
          // payload typed as string
        }
      }
    }

    If you need a refresher on when to use assertions vs. guards vs. narrowing, consult Using Type Assertions vs Type Guards vs Type Narrowing (Comparison).

    8. Controlling Next() Input Types (TNext)

    AsyncIterator<T, TReturn, TNext> allows callers to pass values back into the generator via next(value). This is rarely used in practice but can be powerful:

    ts
    async function* pump<T>(): AsyncGenerator<T, void, T> {
      while (true) {
        const nextValue = yield await Promise.resolve(undefined as unknown as T);
        // nextValue is typed as T
        // process nextValue and maybe yield more
      }
    }

    Most codebases don't use TNext, but if you design an API that accepts feedback values on next(), type them explicitly. For alternate patterns, consider using channels/subjects instead.

    9. Using satisfies and const assertions to help inference

    When you export configuration objects or action maps consumed by async streams, using as const or the satisfies operator can improve literal inference and prevent unwanted widening:

    ts
    const ACTIONS = {
      ADD: 'ADD',
      REMOVE: 'REMOVE',
    } as const;
    
    type ActionKey = typeof ACTIONS[keyof typeof ACTIONS]; // 'ADD' | 'REMOVE'
    
    // Or with satisfies:
    const ACTIONS2 = {
      ADD: 'ADD',
      REMOVE: 'REMOVE',
    } satisfies Record<string, string>;

    Using these features improves downstream types when your async iterator yields messages keyed by constants. Read more about the satisfies operator and when to use const assertions (as const).

    10. Combining Async Iterators with Recursive Data Streams

    Streaming tree traversal can be expressed with async iterators. Typing recursive yielded values and ensuring stack-safety is vital:

    ts
    type TreeNode<T> = { value: T; children?: TreeNode<T>[] };
    
    async function* traverse<T>(root: TreeNode<T>): AsyncGenerator<T, void, unknown> {
      yield root.value;
      for (const c of root.children ?? []) {
        yield* traverse(c);
      }
    }

    This pattern composes nicely with typed recursive structures. For in-depth patterns on recursive types in TypeScript, see Typing Recursive Data Structures in TypeScript: Linked Lists, Trees, and Graphs.

    Advanced Techniques

    • Strictly type the IteratorResult: use explicit return type annotations for next() in custom iterators to prevent incorrect done/value shapes. This avoids the common mistake of returning undefined values when TReturn is not undefined.

    • Use mapped and conditional types to derive item types from higher-level types. For example, given an API response type Response, derive T automatically for your generator.

    • Use runtime guards and branded error types to model rejections and thrown errors. Since TypeScript does not model "throws" in signatures, add typed documentation and runtime predicates.

    • Optimize performance by batching yields. When streaming many small items, consider yielding arrays of items (e.g., T[]) and letting consumer flatten if necessary—this reduces Promise overhead. Make batches opt-in with a generic parameter for batch size.

    • For public libraries, export interfaces like AsyncIterableSource instead of concrete classes to allow alternate implementations (e.g., browser streams vs Node streams).

    Also consider how async iterators interact with third-party libraries; our guide on Typing Third-Party Libraries with Complex APIs — A Practical Guide can help when creating adapters and helper types.

    Best Practices & Common Pitfalls

    • Do: Explicitly annotate your async generator's return and next types when the intent is part of the public API; inference is good for local code but weakens library boundaries.

    • Don't: Assume thrown errors are typed. Provide runtime guards or well-documented error classes so consumers can discriminate. See Typing Error Objects in TypeScript: Custom and Built-in Errors.

    • Do: Prefer AsyncIterable in public types rather than concrete class types. This increases compatibility with language features like for await...of and Node streams.

    • Don't: Return malformed IteratorResult objects. Ensure the "done" and "value" properties align with IteratorResult<T,TReturn> shapes to avoid type mismatches at runtime.

    • Do: Use type guards for heterogeneous yields and discriminated unions to keep consumer code safe.

    • Troubleshooting tip: If for await...of yields values as unknown, inspect your tsconfig libs (ES2018+) and ensure the generator function signatures are recognized by the compiler.

    For more debugging and source-map concerns while working with async flows, refer to Debugging TypeScript Code (Source Maps Revisited).

    Real-World Applications

    • Streaming API clients: Use async iterators to paginate through large API result sets lazily without loading all pages into memory. Combine with precise generics to surface typed items to consumers.

    • Event processing: Model event sources (WebSocket, Server-Sent Events) as AsyncIterable, enabling simple loops with for await...of and typed event payloads.

    • File processing: Parse large files line-by-line or chunk-by-chunk using async iteration, converting each chunk into typed domain objects.

    • Database cursors: Wrap cursor APIs (e.g., MongoDB) in typed async iterators for backpressure-aware consumption.

    If your streaming data comes from Node built-ins, the patterns in Typing Node.js Built-in Modules in TypeScript are especially relevant.

    Conclusion & Next Steps

    Typed async iterators let you express streaming workflows safely and ergonomically in TypeScript. Start by annotating public async generator functions and custom AsyncIterable implementations, then iterate toward more advanced patterns like error typing, batching, and integration with Node streams.

    Next steps: practice by converting a paginated API client to an async generator, add discriminated unions for events, and document error shapes. Explore related topics like type guards and const assertions to improve inference and safety.

    Enhanced FAQ

    Q1: What's the difference between AsyncIterable, AsyncIterator, and AsyncGenerator<T,TReturn,TNext>?

    A1: AsyncIterable is an object that exposes Symbol.asyncIterator returning an AsyncIterator. AsyncIterator<T,TReturn,TNext> is the protocol for the object returned by Symbol.asyncIterator, and it defines async next(): Promise<IteratorResult<T,TReturn>>. AsyncGenerator<T,TReturn,TNext> is a convenience type for async generator functions (async function*), which implement both AsyncIterator and AsyncIterable and allow yield/return/next types. Use AsyncIterable for public parameters and AsyncGenerator when annotating generator functions.

    Q2: How do I type the values returned by next() vs the values yielded?

    A2: The yielded type is T, and the return type (when done is true) is TReturn. The union IteratorResult<T,TReturn> models either { value: T; done: false } or { value: TReturn; done: true }. Annotate your next() method accordingly, and in generator functions annotate as AsyncGenerator<T,TReturn,TNext>. If you rely on void for returns, use void or undefined consistently to avoid awkward casts.

    Q3: Can async iterator functions throw typed errors? How do I reflect that in TypeScript?

    A3: TypeScript has no native way to express thrown types (there's no throws clause). Use well-typed Error subclasses, runtime guards, and explicit documentation. For Promise rejections, you can wrap return types in Result<T, E> or use tagged union returns to encode errors in the yielded stream. See Typing Promises That Reject with Specific Error Types in TypeScript for more strategies.

    Q4: Should public APIs return AsyncIterator or AsyncIterable?

    A4: Prefer AsyncIterable (or AsyncIterableIterator) for public APIs. This lets consumers use for await...of and compose with other iterable utilities. Returning a raw AsyncIterator ties consumers to your implementation and might reduce interop.

    Q5: How can I type an async iterator that streams JSON lines from a file?

    A5: Define an AsyncGenerator<T, void, unknown> that parses lines and yields T. Ensure your parsing uses safely typed JSON parsing (with guards or validators) since JSON.parse yields any. Combine with strategies from Typing JSON Payloads from External APIs (Best Practices) to enforce schema validation.

    Q6: What's a practical way to test my typed async iterator implementations?

    A6: Unit-test the concrete runtime behavior and use TypeScript's type system for compile-time checks. Example tests: ensure yields count and values match expectations, ensure next() resolves to proper IteratorResult shapes, and simulate error cases. Use tools like tsd to assert compile-time types in tests. Also create integration tests that consume iterators with for await...of to validate behavior.

    Q7: Are there performance considerations with async iterators?

    A7: Yes—each yield costs an async boundary (Promise resolution). For high-throughput scenarios, batch multiple items into an array and yield the batch to reduce Promise overhead. Use micro-batching and backpressure-aware producers if the consumer is slower than the producer. Also prefer streaming parsing when handling large payloads.

    Q8: How do I type next() input (TNext) when consumers call next(value)?

    A8: Annotate your generator as AsyncGenerator<T,TReturn,TNext>. When a consumer calls next(value), value will be typed as TNext for the await result inside the generator. This feature is rarely used but useful for coroutine-like patterns. If you don't use it, set TNext to unknown or undefined.

    Q9: How do async iterators interact with garbage collection—do they leak memory?

    A9: Async iterators themselves do not inherently leak memory, but long-lived generators holding closures with large objects can. Ensure you release references and provide explicit close() or return() methods in long-lived streams. Consumers should call return() when aborting iteration to allow the generator cleanup code to run.

    Q10: Where can I learn more about typing patterns related to async iterators (guards, assertions, const assertions)?

    A10: Explore linked resources on related topics: Using Type Assertions vs Type Guards vs Type Narrowing (Comparison), When to Use const Assertions (as const) in TypeScript: A Practical Guide, and the deep dive on Typing Asynchronous Generator Functions and Iterators in TypeScript. These will improve your patterns for robust, well-typed streaming APIs.


    If you enjoyed this tutorial, consider exploring related posts on typing error objects for safer exception handling, and how to type Node.js streams when building server-side streaming features. For a deeper dive into building robust, typed library APIs with static and abstract class type patterns, see our other TypeScript guides.

    article completed

    Great Work!

    You've successfully completed this TypeScript tutorial. Ready to explore more concepts and enhance your development skills?

    share this article

    Found This Helpful?

    Share this TypeScript tutorial with your network and help other developers learn!

    continue learning

    Related Articles

    Discover more programming tutorials and solutions related to this topic.

    No related articles found.

    Try browsing our categories for more content.

    Content Sync Status
    Offline
    Changes: 0
    Last sync: 11:20:00 PM
    Next sync: 60s
    Loading CodeFixesHub...