Type Promise Return Values

Learn to type what a promise resolves to in TypeScript. Covers explicit Promise<T> annotations, inference from async functions, ReturnType, and Awaited for extracting resolved types.

7 min read

A promise return value is what the promise eventually resolves to. In TypeScript, you type this value by declaring Promise<T>, where T is the resolved type. You can annotate it explicitly, let inference figure it out, or derive it from other types using utility types like ReturnType and Awaited.

This article covers all three approaches and when to use each one.

Explicit Promise Annotations

The most direct way to type a promise return value is to annotate the function's return type with the resolved type wrapped in Promise.

typescripttypescript
async function fetchUserName(id: number): Promise<string> {
  const response = await fetch(`/api/users/${id}`);
  const user = await response.json();
  return user.name;
}

The annotation Promise of string tells TypeScript and anyone reading the code that this function eventually produces a string. If you later change the implementation to return a number, TypeScript catches the mismatch at compile time.

Explicit annotations are most valuable on exported functions and public API surfaces. They serve as a contract: callers know exactly what type to expect, and the implementation is held to that contract.

Letting TypeScript Infer the Type

For internal functions and simple cases, you can skip the annotation. TypeScript infers the return type from the actual return statements in the function body.

typescripttypescript
async function fetchCount() {
  return response.headers.get("X-Total-Count");
}

The inferred return type is Promise of (string or null), because the headers get method can return null. Hovering over the function name in your editor shows the inferred type. Since it is derived from the actual code, it always matches reality.

Inference also works across multiple code paths. TypeScript computes the union of all possible return types from every branch. If one branch returns "ok" and another returns 404, the inferred return type becomes Promise of (string or number).

Using ReturnType to Extract Return Types

TypeScript provides the ReturnType utility type to extract the return type from any function signature. This is useful when you want to reference the return type without duplicating it.

typescripttypescript
async function createUser(name: string, email: string) {
  return { id: 1, name, email };
}
 
type CreateUserResult = ReturnType<typeof createUser>;

ReturnType gives you the full return type, which for an async function is a Promise wrapping the object type. You get Promise of the user object, not just the object itself. To get just the resolved value type, you need one more step.

Using Awaited to Get the Resolved Value

Awaited recursively unwraps promises to find the innermost resolved type. Combine it with ReturnType to extract the resolved value from an async function.

typescripttypescript
async function getDashboardData() {
  const [user, stats] = await Promise.all([
    fetchCurrentUser(),
    fetchSiteStats(),
  ]);
  return { user, stats };
}
 
type DashboardData = Awaited<ReturnType<typeof getDashboardData>>;

ReturnType produces Promise of the dashboard object, and Awaited unwraps the Promise layer to give just the object type. You now have a reusable type for the dashboard data without duplicating the shape.

ReturnType and Awaited pipeline

The two-step pipeline extracts the resolved type from any async function. You can reuse the resulting type for variable declarations, component props, generic parameters, or other return type annotations.

Typing Variables That Hold Promises

When you store a promise in a variable before awaiting it, TypeScript infers the variable type from the initializer. If you need to declare a variable without an initializer, you must annotate it with the full Promise type.

typescripttypescript
let deferredPromise: Promise<string>;
 
if (condition) {
  deferredPromise = Promise.resolve("branch A");
} else {
  deferredPromise = Promise.resolve("branch B");
}

The annotation Promise of string constrains the variable so that only promises resolving to strings can be assigned to it. Both branches satisfy this constraint, so the code compiles.

Typing Callbacks That Return Promises

When a callback returns a promise, TypeScript infers the full Promise type. This matters when the callback's return value flows into another generic type.

typescripttypescript
function withRetry<T>(fn: () => Promise<T>): Promise<T> {
  return fn().catch(() => fn());
}
 
const result = withRetry(() => fetchUser(1));

The generic parameter T captures the resolved type of the callback's promise. When you pass a callback that returns Promise of User, T is inferred as User, and the entire chain stays typed. The caller gets back a Promise of User without writing any type annotations.

Common Mistakes

Using raw ReturnType without Awaited. ReturnType of an async function is a Promise, not the resolved value. If you assign it to a variable expecting the resolved value, the types will not match. Always wrap with Awaited when you need the inner type.

Not annotating exported async functions. Inference works, but explicit return types on public APIs prevent accidental type drift. A small change to an internal implementation should not silently change the return type that consumers depend on. For more on how async functions interact with the type system, see async and await in TypeScript.

Forgetting that promises can be nested. Promise of Promise of number is a valid type. Awaited handles this, but manual unwrapping with ReturnType alone does not.

For practical examples of wrapping fetch calls, see build a typed fetch wrapper.

Rune AI

Rune AI

Key Insights

  • Annotate async function return types as Promise of T for public-facing functions.
  • TypeScript infers the resolved type from the return statement inside async functions.
  • Use ReturnType to extract the full return type from any function.
  • Wrap with Awaited to unwrap the promise and get the resolved value type.
  • Inconsistent return types across branches produce compile errors.
RunePowered by Rune AI

Frequently Asked Questions

When should I explicitly annotate a promise return type?

Annotate the return type explicitly on public API functions and exported functions where you want to prevent accidental changes to the resolved type. For internal helpers and private functions, inference is usually enough.

How do I get the resolved type from a function that returns a promise?

Use Awaited<ReturnType<typeof myFunction>>. ReturnType extracts the full return type as a promise, and Awaited unwraps the promise to get the resolved value type. This is the standard pattern for deriving resolved types from async functions.

Does TypeScript warn me if I return the wrong type from an async function?

Yes. If an async function has an explicit return type of Promise<string> and a code path returns a number, TypeScript reports a type error. It also catches missing return statements and inconsistent return types across branches.

Conclusion

Typing promise return values in TypeScript comes down to three patterns: explicit Promise annotations for public APIs, letting inference handle internal code, and using ReturnType with Awaited to derive resolved types from existing functions. Each pattern has its place. Use explicit annotations where stability matters, inference where brevity helps, and utility types where you need to extract or transform resolved types.