Typing Promises & async/await

Reviewed & published by Brayan K

Promise<T> is TypeScript's type for an asynchronous value that arrives later; await unwraps it to a T, every async function returns a Promise, and Awaited<T> peels those Promise layers off in the type system.

Part of the free TypeScript course at LearnCodingFast — hands-on lessons with worked examples and the output they print, plus practice exercises and a quick quiz.

What You'll Learn

💡 Real-World Analogy

A Promise<T> is like a coat-check ticket. You hand over your coat and get a ticket (the promise) immediately — but the ticket isn't the coat. await is redeeming the ticket later to get the actual coat (the T) back. An async function always hands you a ticket, never the coat directly, which is why forgetting to await leaves you holding a ticket where you expected a value.

1. Promise<T> and Unwrapping with await

Any function that resolves a value later is typed to return a Promise<T>, where T is the value it eventually produces. Inside an async function, await pauses until the promise settles and hands you the unwrapped T — so await fetchUser(7) turns a Promise<User> into a User.

interface User { id: number; name: string }

// A function that resolves later is typed with Promise<T>:
function fetchUser(id: number): Promise<User> {
  return new Promise((resolve) => {
    setTimeout(() => resolve({ id, name: "User-" + id }), 10);
  });
}

async function main(): Promise<void> {
  const user: User = await fetchUser(7); // await Promise<User> -> User
  console.log(user.name);
}
// A Promise<T> means "a value of type T that arrives later". In .ts you'd type a
// function returning one as:  function fetchUser(id): Promise<User> { ... }
// At RUNTIME it's a real Promise — await unwraps it to the inner value.

function fetchUser(id) {
  return new Promise((resolve) => {
    setTimeout(() => resolve({ id, name: "User-" + id }), 10);
  });
}

async function main() {
  // 'await' turns Promise<User> into User — the unwrapped value:
  const user = await fetchUser(7);
  console.log(user.id);   // 7
  console.log(user.name); // User-7
}

main();

2. Async Return Types & Awaited<T>

Marking a function async means its return type is always wrapped in a Promise: return 42 gives Promise<number>. To go the other way in the type system — from a Promise type to the value it resolves to — use the built-in Awaited<T>, which even flattens nested promises.

// async functions wrap their return type in Promise automatically:
async function getCount(): Promise<number> {
  return 42; // returning number, function type is Promise<number>
}

// Awaited<T> unwraps Promise (and nested Promises) to the resolved value type:
type C = Awaited<ReturnType<typeof getCount>>; // number

type Deep = Awaited<Promise<Promise<string>>>; // string  (fully flattened)
type Plain = Awaited<number>;                  // number  (no-op on non-promises)

// You almost never write Promise<Promise<...>> by hand, but await/Awaited
// flatten it so you don't have to care.
// An async function ALWAYS returns a Promise. 'return 42' => Promise<number>.
// Awaited<T> "peels off" Promise layers to get the resolved type.

async function getCount() {
  return 42;                 // a plain number, but the function returns Promise<number>
}

async function main() {
  const result = getCount();              // a Promise, NOT 42
  console.log(result instanceof Promise); // true
  const count = await result;             // now it's the number
  console.log(count);                     // 42
  console.log(count + 8);                 // 50

  // Awaited<T> also flattens NESTED promises (Promise<Promise<string>> -> string):
  const nested = Promise.resolve(Promise.resolve("done"));
  console.log(await nested);              // done
}

main();

3. A Typed Fetch Wrapper & Promise.all Tuples

A generic getJSON<T>(url): Promise<T> wrapper lets the call site say what shape it expects — getJSON<User>(url) resolves to a User. And when you await a fixed tuple via Promise.all, TypeScript keeps each position's type, so destructuring gives you correctly typed variables instead of a widened union.

// A typed fetch wrapper — the generic T flows from call site to result:
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;
}

interface User { id: number; name: string }
const user = await getJSON<User>("/api/user/1"); // user: User

// Promise.all preserves the TUPLE positions and their types:
const [a, b]: [string, number] = await Promise.all([
  Promise.resolve("hi"),  // -> string
  Promise.resolve(1),     // -> number
]);
// Promise.all takes a tuple of promises and resolves to a TUPLE of results —
// each slot keeps its own type:  const [a, b] = await Promise.all([f1(), f2()]);

function fetchName() { return Promise.resolve("Ada"); }
function fetchAge()  { return Promise.resolve(36); }
function fetchTags() { return Promise.resolve(["admin", "beta"]); }

async function main() {
  // TypeScript infers: name=string, age=number, tags=string[]
  const [name, age, tags] = await Promise.all([fetchName(), fetchAge(), fetchTags()]);
  console.log(name);             // Ada
  console.log(age + 1);          // 37            (age is a number)
  console.log(tags.join(", "));  // admin, beta   (tags is an array)
}

main();

🎯 Your Turn

Unwrap a promise from a tiny fetch-style function. Fill in the two blanks marked ___, then run it.

// 🎯 YOUR TURN — replace each ___ then press "Try it Yourself".
// In .ts this would be:  function getJson(url): Promise<{ url: string; ok: boolean }>
// Await it to unwrap the resolved value.

function getJson(url) {
  return Promise.resolve({ url, ok: true });
}

async function main() {
  const data = ___ getJson("/api/ping"); // 👉 the keyword that unwraps a Promise
  console.log(data.url); // /api/ping
  console.log(data.___);  // 👉 read the 'ok' property -> true
}

main();

// ✅ Expected output:
//    /api/ping
//    true

Common Errors (and the fix)

Pro Tips

📚 Keep Learning

Mini-Challenge: A Typed Fetch Pipeline

No blanks this time — just a brief and a starting outline. Build the Promise.all pipeline yourself, run it, and check your output against the example in the comments.

// 🎯 MINI-CHALLENGE: a typed fetch pipeline using Promise.all.
// 1. loadProfile(id) resolves to { id, name: "User-" + id }.
// 2. loadPosts(id)   resolves to ["post-1", "post-2"].
// 3. buildDashboard(id): await Promise.all of BOTH, then return
//    profile.name + " has " + posts.length + " posts".
// 4. In main(), await buildDashboard(9) and log it.
//
// ✅ Expected output:
//    User-9 has 2 posts

// your code here

🎉 Lesson Complete

Practice quiz

What does Promise<User> represent?

  • A User that exists right now
  • An array of Users
  • An async operation that will resolve to a User later
  • A User that may be null

Answer: An async operation that will resolve to a User later. Promise<T> is the type of an async operation that will eventually resolve to a value of type T - here, a User.

Inside an async function, what does await fetchUser(7) give you when fetchUser returns Promise<User>?

  • User
  • Promise<User>
  • Promise<Promise<User>>
  • unknown

Answer: User. await unwraps a Promise<T> to its inner value T, so awaiting a Promise<User> yields a User.

An async function does return 42. What is its return type?

  • number
  • 42
  • Promise<void>
  • Promise<number>

Answer: Promise<number>. Every async function wraps its return value in a Promise, so returning a plain number gives the type Promise<number>.

What does the built-in Awaited<T> utility type do?

  • Wraps T in a Promise
  • Recursively unwraps Promise layers to the resolved value type
  • Makes T optional
  • Converts T to an array

Answer: Recursively unwraps Promise layers to the resolved value type. Awaited<T> peels off Promise layers (even nested ones) to give the final resolved value type, mirroring what await does.

What is Awaited<Promise<Promise<string>>>?

  • string
  • Promise<string>
  • Promise<Promise<string>>
  • unknown

Answer: string. Awaited fully flattens nested promises, so Awaited<Promise<Promise<string>>> resolves to string.

What is Awaited<number> (a non-promise type)?

  • Promise<number>
  • never
  • number
  • void

Answer: number. Awaited is a no-op on non-promise types, so Awaited<number> is just number.

What type does await Promise.all([Promise.resolve("a"), Promise.resolve(1)]) produce?

  • (string | number)[]
  • [string, number]
  • string[]
  • Promise<[string, number]>

Answer: [string, number]. Promise.all over a fixed tuple resolves to a tuple keeping each position's type: [string, number].

Why does reading user.name on const user = fetchUser(1) (no await) fail to type-check?

  • fetchUser doesn't exist
  • name is private
  • fetch returns void
  • user is a Promise<User>, not a User

Answer: user is a Promise<User>, not a User. Without await, user is a Promise<User>, which has no .name property - a very common bug. Add await first.

Why can't you use await directly in a non-async function?

  • await only works at the top level
  • await is only allowed inside async functions (or top-level module await)
  • await requires a try/catch
  • await needs a Promise.all

Answer: await is only allowed inside async functions (or top-level module await). 'await' expressions are only allowed within async functions (or as top-level await in a module). Mark the function async.

How do you grab the resolved result type of an async function fn for reuse?

  • ReturnType<typeof fn>
  • Promise<typeof fn>
  • Awaited<ReturnType<typeof fn>>
  • typeof fn.return

Answer: Awaited<ReturnType<typeof fn>>. ReturnType<typeof fn> is the Promise; wrapping it in Awaited<...> unwraps it to the resolved value type.

Continue this course

Frequently asked questions

What does Promise<T> mean?

Promise<T> is the type of an asynchronous operation that will eventually resolve to a value of type T. For example, Promise<User> is a promise that will produce a User. You get the User out by awaiting the promise (in an async function) or by calling .then(user => ...).

Why does my async function return Promise<number> when I just 'return 42'?

Every async function wraps its return value in a Promise automatically. Returning a plain number gives the function the type Promise<number>, and the caller must await it (or .then it) to access the number. This is why 'const x = getCount()' gives you a Promise, not 42.

What is Awaited<T>?

Awaited<T> is a built-in utility type that recursively 'unwraps' Promise layers to give the final resolved value type. Awaited<Promise<string>> is string, and even Awaited<Promise<Promise<number>>> is number. It models exactly what the 'await' keyword does at runtime, and is handy with ReturnType to get an async function's resolved result.

How are the results of Promise.all typed?

When you pass a fixed tuple of promises to Promise.all, TypeScript resolves it to a tuple of their resolved types, position by position. So 'await Promise.all([Promise.resolve("a"), Promise.resolve(1)])' has type [string, number] — destructuring keeps each element correctly typed, unlike a plain array which would widen to a union.

Related lessons