Typing promises: Promise<T> and what it guarantees
Most real TypeScript is asynchronous — fetching data, reading files, querying a database, calling an
API. All of it runs on promises, and TypeScript types them precisely with Promise<T>. Getting the
types right here is not optional polish; it is how you avoid the "I awaited the wrong thing" and "this
was still a promise" bugs that async code is full of. This module is typed async, and it starts with the
promise itself.
Promise<T> — a future value of type T
A Promise<T> represents a value of type T that will be available later. The <T> is the type of
what the promise will eventually resolve to:
const p1: Promise<string> = Promise.resolve("Kavita"); // will resolve to a string
const p2: Promise<number> = Promise.resolve(42); // will resolve to a number
const p3: Promise<User> = fetchUser(1); // will resolve to a User
Promise<string> is "a promise that will produce a string". It is a generic type (from the generics
module) — Promise is generic in the type of its eventual value. This matters because it lets TypeScript
track what you will get when the promise completes, all the way through your async code.
The crucial thing to internalise: a Promise<string> is not a string. It is a box that will
contain a string later. You cannot use the string until you unwrap the promise (with await or
.then), and TypeScript enforces this:
const p: Promise<string> = Promise.resolve("Kavita");
p.toUpperCase(); // error: Property 'toUpperCase' does not exist on type 'Promise<string>'
p.toUpperCase() fails because p is a Promise, not a string — the string is inside, not yet
available. This error — trying to use a promise as if it were the resolved value — is one of the most
common async mistakes, and TypeScript catches it every time. When you see "does not exist on type
Promise<...>", the fix is almost always "you forgot to await it".
Consuming a promise with .then
The original way to get the value out is .then, and TypeScript types the callback for you:
const p: Promise<string> = Promise.resolve("Kavita");
p.then((name) => {
name.toUpperCase(); // name is inferred as string — the resolved value
});
.then takes a callback that receives the resolved value, and TypeScript infers its type from the
Promise<T> — name is a string, because p is a Promise<string>. .then can also transform,
returning a new promise:
const lengthPromise: Promise<number> = p.then((name) => name.length);
The callback returns a number, so .then produces a Promise<number>. Chaining .then builds a
pipeline of promises, each typed. You will mostly use async/await (next lesson) instead of .then
chains — it reads far better — but .then is the underlying mechanism, and you will meet it.
Creating a typed promise
When you wrap a callback-based API or build your own async operation, you construct a promise, and you annotate what it resolves to:
function delay(ms: number): Promise<void> {
return new Promise((resolve) => {
setTimeout(resolve, ms); // resolves with no value -> Promise<void>
});
}
function readValue(): Promise<string> {
return new Promise((resolve, reject) => {
// resolve("data") to succeed with a string; reject(new Error(...)) to fail
resolve("data");
});
}
new Promise<T>(...) creates a promise; the resolve function's argument type is T. Promise<void>
is a promise that completes but produces no value (like delay — you await it for the timing, not a
result). Annotate the return type (Promise<string>), and TypeScript checks that you resolve with the
right type.
The rejection type — the honest gap
Here is an important limitation to know up front: Promise<T> types the success value, but not the
rejection value. A promise can reject (fail), but TypeScript does not track what type it rejects
with:
const p: Promise<string> = someOperation();
// p resolves with a string — but if it rejects, TypeScript has NO idea what the error type is
This is because JavaScript lets you reject with anything (an Error, a string, an object, undefined
— though you should always use an Error), and there is no type-level way to constrain it. So a
Promise<T> tells you the happy-path type but says nothing about failure — which is why the
async-error-handling lesson (later in this module) is important: you handle the error type yourself,
because the promise type will not do it for you. This gap is the async equivalent of "TypeScript does not
validate external data" — a boundary where you must add care the types cannot provide.
Why the promise type matters
Getting Promise<T> right is the foundation of all typed async code:
- It tracks what you will get — so the moment you
await, TypeScript knows the exact type of the result. - It catches the "used a promise as a value" bug — forgetting to
awaitis a compile error, not a runtime surprise ([object Promise]in your UI, orundefinedwhere you expected data). - It flows through the whole async pipeline —
.thenchains,Promise.all,asyncfunctions — keeping types precise at every step.
The async/await syntax in the next lesson makes promises pleasant to work with, but it is all built
on Promise<T> — so understanding that a promise is a typed box around a future value is the mental
model everything else rests on.
Check your work
What Promise<T> represents. A value of type T that will be available later — T is what it
resolves to.
Is a Promise<string> a string? No — it is a box that will contain a string later; you cannot use
the string until you unwrap the promise.
What error signals a forgotten await. "Property ... does not exist on type Promise<...>" — you
used the promise as if it were the resolved value.
How .then is typed. Its callback receives the resolved value (inferred from Promise<T>), and it
returns a new promise of whatever the callback returns.
What Promise<void> is. A promise that completes but produces no value.
How to create a typed promise. new Promise<T>(...), annotating what it resolves to.
The rejection-type gap. Promise<T> types the success value but not the rejection — TypeScript
does not track what a promise rejects with, so you handle error types yourself.
Why the promise type matters. It tracks what you will get, catches the "used a promise as a value" bug, and flows types through the whole async pipeline.
Practice
- Declare a
Promise<string>withPromise.resolve. Try to call a string method on it directly and read the error. - Consume it with
.thenand confirm the callback's parameter is astring. - Chain a
.thenthat returns anumberand confirm the result is aPromise<number>. - Write a
delay(ms): Promise<void>wrappingsetTimeout. Confirm its type. - Write a function returning
Promise<string>that resolves with a string, and confirm annotating the wrong resolve type errors. - Note that a promise's rejection type is not tracked — reject with an
Errorand observe TypeScript says nothing about its type. - Take a place where you forgot an
await(or write one) and confirm the "does not exist on Promise<...>" error points you to it.
Official documentation
- TypeScript — Promises and async — Typing promises.
- MDN — Promise — The underlying JavaScript object.
- TypeScript — lib.es2015.promise — And
Awaited, for unwrapping.
Next: async/await, and the types it infers for you.
Stuck on this lesson?
Being stuck is part of it — but being stuck alone for three days is not. Our internship programme pairs this curriculum with code review and one-to-one help from working developers, and it is free.
About the internship