TypeScript Async and Fetch Types

TypeScript async functions always return a Promise<T>, where T is the type of the value you return. Typing promises and API responses correctly is one of the most useful things TypeScript does in real applications.

Promise Return Types

Inside then or after await, TypeScript knows the value is a number.

const wait = (ms: number): Promise<void> => new Promise((r) => setTimeout(r, ms));

async function getScore(): Promise<number> {
  await wait(100);
  return 87;
}
getScore().then((s) => console.log("Score:", s + 3));
Score: 90

Typing a Fetch Response

response.json() returns Promise<any>, so TypeScript cannot check it. Give the result a type in a small helper, ideally with runtime validation for untrusted APIs.

type Todo = { id: number; title: string; completed: boolean };

async function getJson<T>(url: string): Promise<T> {
  const res = await fetch(url);
  if (!res.ok) throw new Error(`HTTP ${res.status}`);
  return (await res.json()) as T;
}

const todo = await getJson<Todo>("https://jsonplaceholder.typicode.com/todos/1");
console.log(todo.id, todo.completed);
1 false

Errors Are unknown

In strict mode, the catch variable has type unknown because anything can be thrown. Narrow it before reading message.

async function safeLoad() {
  try {
    throw new Error("Timeout after 5s");
  } catch (err) {
    const msg = err instanceof Error ? err.message : String(err);
    console.log("Failed:", msg);
  }
}
safeLoad();
Failed: Timeout after 5s

A Typed Result Pattern

Returning a discriminated union instead of throwing makes callers handle both outcomes explicitly.

type Result<T> = { ok: true; value: T } | { ok: false; error: string };

async function parseAge(input: string): Promise<Result<number>> {
  const n = Number(input);
  return Number.isInteger(n) && n > 0 ? { ok: true, value: n } : { ok: false, error: "Invalid age" };
}

for (const input of ["29", "abc"]) {
  const r = await parseAge(input);
  console.log(r.ok ? `Age ${r.value}` : r.error);
}
Age 29
Invalid age

Parallel Requests

Promise.all keeps the type of each position, returning a typed tuple.

const [count, label] = await Promise.all([
  Promise.resolve(42),
  Promise.resolve("items"),
]); // count: number, label: string
console.log(count, label.toUpperCase());
42 ITEMS

Frequently asked questions

How do I type an async function in TypeScript?

Annotate the return type as Promise<T>, for example async function load(): Promise<User[]>. TypeScript can also infer it.

Why is the error in catch of type unknown?

JavaScript can throw any value, not just Error objects. With useUnknownInCatchVariables (part of strict), TypeScript makes you check the type first.

How do I type the JSON returned by fetch?

Assign it to a declared type or use a generic helper. For data you do not control, validate it at runtime with a schema library such as Zod.