Reading — step 1 of 6
Learn
TypeScript's type system shines for async code — Promise<T> makes the resolved type visible at every step, catching bugs that JS lets slip.
Typing a Promise
async function fetchUser(id: number): Promise<User> {
const response = await fetch(`/api/users/${id}`);
return response.json(); // Type checker doesn't know what shape this is — defaults to any
}
The return type Promise<User> documents the resolved value. Inside the function, callers see the type. async functions ALWAYS return a Promise — even async function f() { return 1; } returns Promise<number>.
Typing fetch responses
The built-in fetch returns Response whose .json() is typed as Promise<any>. To get types, use a generic helper:
async function fetchJson<T>(url: string): Promise<T> {
const r = await fetch(url);
if (!r.ok) throw new Error(`HTTP ${r.status}`);
return r.json() as Promise<T>;
}
const user = await fetchJson<User>('/api/users/42');
// ^ User
This pattern (or libraries like Zod for runtime validation) gives end-to-end types for API calls.
Result types — explicit success/failure
For APIs that can fail in expected ways, model failure as a value rather than an exception:
type Result<T, E = Error> =
| { ok: true; value: T }
| { ok: false; error: E };
async function safeFetch<T>(url: string): Promise<Result<T>> {
try {
const data = await fetchJson<T>(url);
return { ok: true, value: data };
} catch (e) {
return { ok: false, error: e instanceof Error ? e : new Error(String(e)) };
}
}
const r = await safeFetch<User>('/api/me');
if (r.ok) {
console.log(r.value.name); // narrowed to User
} else {
console.error(r.error.message); // narrowed to Error
}
The discriminated union (ok: true | false) lets the compiler narrow inside each branch — no casts needed.
Promise.all and tuple types
const [user, posts]: [User, Post[]] = await Promise.all([
fetchJson<User>('/api/users/42'),
fetchJson<Post[]>('/api/users/42/posts'),
]);
TypeScript infers the tuple type from the array of differently-typed Promises. Each parallel call's resolved type is preserved.
Promise.allSettled — every outcome
type Settled<T> = { status: 'fulfilled'; value: T } | { status: 'rejected'; reason: unknown };
const results = await Promise.allSettled([
fetchJson<User>('/api/a'),
fetchJson<User>('/api/b'),
]);
// results: PromiseSettledResult<User>[]
for (const r of results) {
if (r.status === 'fulfilled') {
console.log(r.value.name);
}
}
Useful when you want every result regardless of failure — analytics, batch jobs, etc.
Awaiting in loops vs Promise.all
// SLOW — sequential
for (const id of ids) {
const user = await fetchUser(id);
console.log(user);
}
// FAST — parallel
const users = await Promise.all(ids.map(fetchUser));
for (const user of users) console.log(user);
Same trap as in plain JS — if items don't depend on each other, kick them all off, then await.
Common mistakes
- Forgetting that
asyncalways returns a Promise — TypeScript's type narrows it for you. - Awaiting in a loop when independent items could run in parallel — silently slow.
- Catching errors as
any— modern TS treatscatch (e)asunknown. Narrow withinstanceof Erroror use a type guard. - Using
.then().catch()chains in TS — async/await is cleaner and types better.
Discussion
Ask a question, share an insight, or help someone who’s stuck.
Sign in to post a comment or reply.
Loading…