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".
// 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.
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:
// 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.
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
- A function that does async work returns a
Promise<T>, whereTis the value it will produce. - Mark the function
asyncso you can useawaitinside it. - Use
awaiton a promise to pause and receive the plainTvalue (no.thenneeded). - For
fetch,await res.json()givesany— assert or validate it to your own interface to regain type safety. - Remember the whole async function itself returns a
Promise, so its callersawaitit 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>?
✍️ Practice
- Write an async function
delayHi(): Promise<string>that resolves to a greeting, then await and log it. - Define a
Userinterface and a typedloadUser(id: number): Promise<User>that asserts the fetched JSON as User.
🏠 Homework
- 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().