RizTech Academy logo
RizTech Academy
Asynchronous TypeScriptLesson 1 of 525 min

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 await is a compile error, not a runtime surprise ([object Promise] in your UI, or undefined where you expected data).
  • It flows through the whole async pipeline — .then chains, Promise.all, async functions — 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

  1. Declare a Promise<string> with Promise.resolve. Try to call a string method on it directly and read the error.
  2. Consume it with .then and confirm the callback's parameter is a string.
  3. Chain a .then that returns a number and confirm the result is a Promise<number>.
  4. Write a delay(ms): Promise<void> wrapping setTimeout. Confirm its type.
  5. Write a function returning Promise<string> that resolves with a string, and confirm annotating the wrong resolve type errors.
  6. Note that a promise's rejection type is not tracked — reject with an Error and observe TypeScript says nothing about its type.
  7. 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

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