Skip to Content
Conditional Types That Extract a Promise's Value
Advanced
0 likes
Tutorials

Conditional Types That Extract a Promise's Value

Write a conditional type with infer that takes the value out of a promise and see how it differs from Awaited. After this, you can read the value type of an async function without writing it by hand.

Skip ahead: If you only need the one-line answer, use Awaited<ReturnType<typeof yourFunction>> and stop here. Read on if you want to know why, or if you need to write the type yourself.

You have an async function and you need the type of what it eventually gives you. Not Promise<User>, just User. Copying the shape by hand works until the function changes. A conditional type with infer pulls the value out of the promise and keeps it in sync. This page builds it in four small steps, checks it, and then shows where the built-in does the same job.

Step 1: the function

export async function loadUser(id: number) {
  return { id, name: "Ana", active: true };
}

Nothing is annotated. The return type is inferred as Promise<{ id: number; name: string; active: boolean }>. That is the type you want to unwrap.

Step 2: take the value out of one promise

import { loadUser } from "./load.js";

type Unwrap<T> = T extends Promise<infer V> ? V : T;

type User = Unwrap<ReturnType<typeof loadUser>>;

const user: User = { id: 1, name: "Ben", active: false };
console.log(user.name);

T extends Promise<infer V> asks whether T is a promise of something, and infer V names that something. If it is, you get V. If not, you get T back unchanged, so a plain value passes through. ReturnType<typeof loadUser> gives you the promise, because typeof loadUser is the type of the function.

Step 3: test it so it stays right

A type that nobody checks drifts. Add a test that fails to compile when the answer changes.

import { loadUser } from "./load.js";

type Equal<X, Y> =
  (<T>() => T extends X ? 1 : 2) extends (<T>() => T extends Y ? 1 : 2) ? true : false;

type Unwrap<T> = T extends Promise<infer V> ? V : T;
type User = Unwrap<ReturnType<typeof loadUser>>;

const same: Equal<User, { id: number; name: string; active: boolean }> = true;
const wrong: Equal<User, { id: string }> = true;
test_unwrap.ts(10,7): error TS2322: Type 'true' is not assignable to type 'false'.

The first line passes silently. The second shows what a failing test looks like. Equal is the usual trick for comparing two types exactly, and you can paste it as it is.

Step 4: a promise inside a promise

type Unwrap<T> = T extends Promise<infer V> ? V : T;

const value: Unwrap<Promise<Promise<number>>> = 1;
nested.ts(3,7): error TS2322: Type 'number' is not assignable to type 'Promise<number>'.

One level only. The result is Promise<number>, not number. A real await flattens nested promises, so your type should too. Make the type call itself on what it found.

type Deep<T> = T extends PromiseLike<infer V> ? Deep<V> : T;

declare function nested(): Promise<Promise<number>>;

type Result = Deep<ReturnType<typeof nested>>;
const answer: Result = 42;
console.log(answer);

nested is only declared, because a real promise flattens itself at run time, so a layered type shows up in declarations and generic code, not in values you build by hand. Two changes. The type now recurses with Deep<V>, so any number of layers collapses. And it checks PromiseLike instead of Promise, which is the shape await actually accepts: anything with a then method.

The built-in does the same

The standard library has this type already, and it handles the cases above plus a few edge cases such as null and undefined inside the promise.

import { loadUser } from "./load.js";

type Equal<X, Y> =
  (<T>() => T extends X ? 1 : 2) extends (<T>() => T extends Y ? 1 : 2) ? true : false;

type Deep<T> = T extends PromiseLike<infer V> ? Deep<V> : T;

const ok: Equal<Deep<Promise<Promise<number>>>, Awaited<Promise<Promise<number>>>> = true;
const user: Awaited<ReturnType<typeof loadUser>> = { id: 1, name: "Cy" };
builtin.ts(9,7): error TS2741: Property 'active' is missing in type '{ id: number; name: string; }' but required in type '{ id: number; name: string; active: boolean; }'.

Awaited agrees with the hand-written version on nested promises, and the second error shows active is required. For application code, use Awaited<ReturnType<typeof fn>> and move on. Write your own only when you need a different rule, such as stopping after one level, or unwrapping a type of your own that is not a promise.

Common mistakes

  • Using Promise in the check when the library returns a PromiseLike. Test with the type your code really gets.
  • Forgetting that the result of ReturnType is the promise. Unwrap<typeof loadUser> checks the function itself and returns the function type unchanged.
  • Passing a function that is not async and expecting the promise to appear. ReturnType gives exactly what the function declares.

Related course: TypeScript Advanced.

About

Write a conditional type with infer that takes the value out of a promise and see how it differs from Awaited. After this, you can read the value type of an async function without writing it by hand.

Responsible
John G
Last Update
10/10/2026
Reading Time
4 min
Was this helpful?
​ 0 ​ 0

Comments (0)

Sign in to comment

No comments yet.