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.
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:
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:
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.
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.
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.
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:
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.
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:
| Method | Return type behavior |
|---|---|
| Promise.all | Tuple matching each input position's type |
| Promise.race | Union of all input types |
| Promise.allSettled | Array of settled result objects |
| Promise.resolve | Promise wrapping the given value's type |
| Promise.reject | Promise 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
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.
Frequently Asked Questions
What does Promise<T> mean in TypeScript?
Can TypeScript infer the type of a promise automatically?
How do I type the error in a catch block?
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.
More in this topic
TypeScript Compiler Explained
The TypeScript compiler (tsc) turns .ts files into .js files while checking types. Learn what it does, how to use it, and how it fits your workflow.
Set Up ESLint for TypeScript
Install and configure ESLint with typescript-eslint to catch bugs and enforce consistent code style in your TypeScript project.
Pick Utility Type in TypeScript
Pick creates a new type by selecting only the properties you name from an existing type. Use it to build focused types for specific views, API responses, or function parameters.