Going DeeperExtra· 45 min read

Typing Async Code: Promises, async/await & fetch

Almost every TypeScript job involves fetching data. Learn how types flow through Promises, async/await, and a typed fetch call.

What you will learn

  • Read and write the Promise<T> type
  • Type an async function and what await gives you
  • Type a real fetch call and its JSON result

Async, in one sentence

Some work — downloading data, reading a file, waiting for a timer — does not finish instantly. Asynchronous (async) code starts that work and lets the rest of your program continue, delivering the result *later*. In JavaScript and TypeScript, "a value that will arrive later" is represented by a Promise.

Promise<T> — a value coming later, of type T

A Promise<T> is a generic type (you met generics earlier). The T is the type of the value the promise will eventually produce. Promise<number> means "a number, later"; Promise<string[]> means "a list of strings, later".

Promise<number> tells TypeScript the future value is a number
// A function that promises to return a number later
function getScore(): Promise<number> {
  return Promise.resolve(95);
}

getScore().then(score => {
  console.log(score + 5);   // score is typed as number
});

Note: Output: 100 The return type Promise<number> means the eventual value is a number, so inside .then, score is typed number and score + 5 is valid. Promise a string instead and the maths would error.

async/await — the readable way

Chaining .then gets messy. The async/await keywords let you write async code that *reads* like normal step-by-step code. Marking a function async means it always returns a Promise; await pauses until a promise resolves and hands you the plain value inside.

await unwraps Promise<number> into a plain number
function getScore(): Promise<number> {
  return Promise.resolve(95);
}

async function showScore(): Promise<void> {
  const score = await getScore();   // score is a plain number here
  console.log('Score is ' + score);
}

showScore();

Note: Output: Score is 95 await getScore() waits for the Promise<number> and gives back a plain number — so score has type number, not Promise<number>. The function is marked async and returns Promise<void> because it produces no value of its own.

A key rule: async functions always return a Promise

Even if you return a plain value from an async function, the function’s type is a Promise of that value. TypeScript wraps it for you:

A returned string becomes Promise<string>; await unwraps it
// We return a string, but the type is Promise<string>
async function getName(): Promise<string> {
  return 'Asha';
}

async function main(): Promise<void> {
  const name = await getName();   // unwrapped back to string
  console.log(name.toUpperCase());
}

main();

Note: Output: ASHA Even though getName returns the plain string 'Asha', its type is Promise<string> — async always wraps the result in a promise. await then unwraps it back to a string, so .toUpperCase() is allowed.

Typing a real fetch call

The big payoff is typed data fetching. fetch returns a Response; calling .json() on it returns Promise<any> — untyped! We give it a type by declaring an interface for the data and telling TypeScript what the JSON should be.

A fetch call typed end to end with a Todo interface
interface Todo {
  id: number;
  title: string;
  completed: boolean;
}

async function loadTodo(id: number): Promise<Todo> {
  const res = await fetch('https://example.com/todos/' + id);
  const data = (await res.json()) as Todo;   // tell TS the shape
  return data;
}

async function main(): Promise<void> {
  const todo = await loadTodo(1);
  console.log(todo.title, '-', todo.completed ? 'done' : 'pending');
}

main();

Note: Output: Buy milk - pending (Sample data.) loadTodo is declared Promise<Todo>, and we assert the parsed JSON as Todo. From there on, todo.title and todo.completed are fully typed — autocomplete works and a typo like todo.titel is caught.

How types flow through async, in order

  1. A function that does async work returns a Promise<T>, where T is the value it will produce.
  2. Mark the function async so you can use await inside it.
  3. Use await on a promise to pause and receive the plain T value (no .then needed).
  4. For fetch, await res.json() gives any — assert or validate it to your own interface to regain type safety.
  5. Remember the whole async function itself returns a Promise, so its callers await it too.

Tip: Define an interface for any data you fetch, and type your loader as Promise<ThatType>. Suddenly the entire chain — the loader, the await, and every use of the result — is checked and autocompleted, which is the main reason teams adopt TypeScript for API work.

Watch out: await only works inside an async function (or at the top level of a module). Using await in a plain, non-async function is an error. And remember res.json() is any until you give it a type — that is the one spot where unchecked data sneaks in, so always assert or validate it.

Q. Inside an async function, what does const x = await getValue() give you if getValue returns Promise<string>?

Answer: await pauses until the promise resolves and hands you the value inside it. So awaiting a Promise<string> gives a plain string, fully typed.

✍️ Practice

  1. Write an async function delayHi(): Promise<string> that resolves to a greeting, then await and log it.
  2. Define a User interface and a typed loadUser(id: number): Promise<User> that asserts the fetched JSON as User.

🏠 Homework

  1. Write a typed loader for a list: loadTodos(): Promise<Todo[]> that fetches and asserts an array of Todo, then an async main that prints how many are completed. Note where you regained type safety after .json().
Want to learn this with a mentor?

CodingClave runs guided, project-based training (28-day, 45-day & 6-month batches).

Explore Training →