Promises in TypeScript Explained

Learn how TypeScript types promises so you can write async code with confidence. Covers Promise<T>, then/catch chains, and how the compiler catches common async mistakes.

6 min read

A promise in TypeScript is a JavaScript Promise with a type parameter that describes what value it will eventually produce. The type is written as Promise<T>, where T represents the type of the resolved value. A Promise of string means the promise resolves to a string. A Promise of number means it resolves to a number.

This type parameter is not just documentation. TypeScript uses it to check every then callback, every await expression, and every chained operation at compile time, before the promise ever runs.

How Promise Types Work

The Promise type comes from TypeScript's standard library as a generic interface. You can annotate variables and function returns with it to tell the compiler what value to expect when the promise settles:

typescripttypescript
const stringPromise: Promise<string> = Promise.resolve("hello");
const numberPromise: Promise<number> = Promise.resolve(42);

Each line declares the resolved type. TypeScript remembers this and enforces it everywhere the promise is used. If you try to treat the result as the wrong type, the compiler catches the mismatch before runtime.

In most cases you do not need to write the annotation yourself. TypeScript infers the type parameter from the value passed to resolve. The inference is automatic and accurate:

typescripttypescript
const inferred = Promise.resolve("TypeScript figures this out");

The compiler sees that the value is a string, so it infers a promise that resolves to a string. The same inference works with the Promise constructor. TypeScript reads the argument type passed to resolve and sets the type parameter accordingly.

Promise type flow through resolve

The diagram shows the two paths the resolved type takes: through then callbacks and through await expressions. In both cases, the value you work with carries the exact type the promise resolved with.

How Types Flow Through then Chains

When you call then on a promise, the callback receives the resolved value with its correct type. The new promise produced by then gets its type from whatever the callback returns.

typescripttypescript
const promise = Promise.resolve(42);
 
const chained = promise.then((value) => {
  return value.toString();
});

The original promise resolves to a number. The callback receives that number and returns a string. TypeScript infers the chained promise now resolves to a string. This tracking continues through every then in the chain, so each step knows exactly what type it receives and what it produces.

If a then callback itself returns a promise, TypeScript automatically unwraps one level. Returning a Promise of number from inside a then callback produces a chained promise of number, not a nested Promise of Promise of number. This matches the runtime behavior where then flattens returned promises automatically.

Typing the Error Path

The catch method handles rejected promises. Unlike a synchronous try/catch block, TypeScript's built-in type for Promise.prototype.catch types the rejected reason as any, not unknown, even with strict mode enabled.

typescripttypescript
Promise.reject(new Error("Something went wrong"))
  .catch((error) => {
    console.log(error.message);
  });

This compiles with no error at all. Because error is any, the compiler will not stop you from accessing message, calling it as a function, or making any other unsafe access, so a mistake here would only show up at runtime.

JavaScript still allows throwing any value, not just Error objects, so it is worth narrowing before you trust the shape. Use instanceof to check whether the thrown value is an Error object before reading its properties:

typescripttypescript
Promise.reject(new Error("Something went wrong"))
  .catch((error) => {
    if (error instanceof Error) {
      console.log(error.message);
    }
  });

Here the compiler does not require this check, but it protects you from a runtime crash if something other than an Error gets rejected. A synchronous try/catch is different: TypeScript types that caught variable as unknown, and does reject unchecked property access. The next article covers that distinction with async/await.

For more on handling errors safely, see handle API errors in TypeScript.

Promise Static Methods and Their Types

TypeScript provides precise return types for each static Promise method. Understanding these helps you write correct async code without manual type assertions.

Promise.all takes an array of promises and returns a single promise that resolves to a tuple of all resolved values. Each position in the tuple keeps the type of its corresponding input promise.

typescripttypescript
const results = await Promise.all([
  Promise.resolve("Alex"),
  Promise.resolve(30),
]);

The first element is a string and the second is a number. You can destructure the result and TypeScript preserves the types correctly.

Promise.race returns the type of whichever input settles first. Since any input could win, the return type is the union of all input types. If all inputs return the same type, the result is that single type.

Promise.allSettled returns an array of result objects, each with a status field and either a value or reason property. You narrow on the status to access the correct data for fulfilled versus rejected promises.

Here is a quick reference:

MethodReturn type behavior
Promise.allTuple matching each input position's type
Promise.raceUnion of all input types
Promise.allSettledArray of settled result objects
Promise.resolvePromise wrapping the given value's type
Promise.rejectPromise that rejects immediately with the given reason

The Awaited Utility Type

TypeScript 4.5 introduced the Awaited utility type, which recursively unwraps promises to find the innermost resolved type. Given a Promise of string, Awaited gives you string. Given a Promise of Promise of number, Awaited still gives you number by unwrapping both layers.

This is useful when extracting the resolved type from a function that may or may not return a promise. Rather than manually unwrapping nested Promise types, you use Awaited to get the final value type in one step. It also works with unions, so Awaited of (boolean or Promise of number) produces (boolean or number).

Common Mistakes

Assuming .catch() blocks unsafe access. The reason parameter in .catch() is typed any, so the compiler will not flag unchecked property access. Narrow with instanceof or a type guard anyway, since JavaScript can reject with any value.

Assuming then returns the same type. Each then creates a new promise whose type depends on the callback's return value. A callback that returns a boolean produces a Promise of boolean, regardless of the original type.

Mixing await and then in the same function. Both patterns work correctly, but mixing them makes the type flow harder to follow. Pick one pattern per function.

For a deeper look at async/await syntax with types, see async and await in TypeScript.

Rune AI

Rune AI

Key Insights

  • A generic type parameter represents what value a promise resolves to.
  • TypeScript infers the resolved type from the resolve call or async function return.
  • Then chains preserve the type across callbacks but track changes at each step.
  • The reason parameter in .catch() is typed any, not unknown, even in strict mode.
  • The Awaited utility type unwraps nested promises to their innermost value type.
RunePowered by Rune AI

Frequently Asked Questions

What does Promise<T> mean in TypeScript?

Promise<T> is a generic type where T is the type of the value the promise resolves to. A Promise<string> will eventually produce a string. TypeScript uses this type parameter to check what you do with the resolved value in .then() callbacks and after await.

Can TypeScript infer the type of a promise automatically?

Yes. When you call an async function or return a value from new Promise(resolve => resolve(value)), TypeScript infers the type parameter. You rarely need to write Promise<T> explicitly unless you are declaring a function return type or a variable with no initializer.

How do I type the error in a catch block?

The reason parameter in .catch() is typed any, so the compiler will not stop unsafe access on it. A synchronous try/catch block is different: TypeScript types that caught variable as unknown when strict mode is on. In both cases, narrow with instanceof or a type guard before trusting the error's shape, since any value can be thrown in JavaScript.

Conclusion

TypeScript types promises through a generic interface where the type parameter represents the resolved value. The compiler tracks this type through then chains, catch handlers, and async functions. The reason parameter in .catch() stays any, while a synchronous try/catch types its caught variable as unknown, so narrowing before use matters either way. Understanding how the resolved type flows through your async code lets TypeScript catch mistakes before your promises even settle.