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> is a generic type whose parameter is the resolved value. Declaring a function async wraps its return value automatically:
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).
Wrapping callback-style APIs still happens. Give the constructor its type argument so resolve is checked:
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.
| 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.
Awaited<T> computes what await would produce, recursively unwrapping nested promises. It is what you need when deriving a type from an async function:
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 }[]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:
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.
Streams, paginated APIs and readline interfaces expose AsyncIterable<T>. for await consumes them with full typing:
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);await and passing a Promise<User> where a User is expected. The error message names the Promise type; read it.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(...)).What is the return type of `async function f() { return 42; }`?
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.response.json() as unknown and validate with a guard or schema before trusting it.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.