Chapter 9.1☕ 14 min read

Typing Promises

Promise<T> guarantees the type of your async data, like a chai token guarantees chai.

01The Chai Token Promise

JavaScript is fundamentally asynchronous. Whether you are fetching data from an API, reading a file, waiting for a timer, or processing user input — you work with asynchronous operations constantly. Promises are the backbone of asynchronous JavaScript, and TypeScript makes them type-safe with the Promise<T> generic type.

Think of a Hyderabadi Irani chai hotel. When you walk in and ask for chai, the waiter doesn't instantly hand it to you. He gives you a token. That token represents a future chai. You KNOW — with absolute certainty — that when you present that token, you will get chai, not biryani, not haleem, not coffee. The token guarantees the result: chai.

Promise<T> is that token. T is the type of what you will eventually get. Promise<string> is a token that will give you a string. Promise<User> is a token that will give you a User object. And just like the chai token ensures you won't get biryani by mistake, Promise<T> ensures TypeScript knows exactly what type to expect when the promise resolves. No guessing, no runtime crashes, no surprises — just type-safe async data flowing through your application.

02Promise<T> Basics

The core type is Promise<T>, where T is the type that the promise resolves to. Let's see it in action with a simple example:

function wait(ms: number):
  Promise<void> {
  return new Promise(resolve =>
    setTimeout(resolve, ms)
  );
}

// Promise<void> — resolves
// with no value (undefined)

The most common way to interact with a promise's type is through .then(). The callback in .then() receives exactly the type T from Promise<T>:

function fetchName():
  Promise<string> {
  return Promise.resolve("Imran");
}

fetchName().then(name => {
  // name is string here!
  console.log(name.toUpperCase());
  // TypeScript knows name is string
});

fetchName().then(name => {
  // This would be a TYPE ERROR:
  // name.toFixed(2)
  // Property 'toFixed' does not exist
  // on type 'string'
});

For errors, TypeScript types the .catch() callback parameter as unknown. This is deliberate — JavaScript allows rejecting a promise with any value, not just Error objects:

fetchName()
  .then(name => console.log(name))
  .catch(err => {
    // err is unknown here
    // You MUST narrow it!
    if (err instanceof Error) {
      console.log(err.message);
    }
  });

// The finally callback doesn't
// receive any value:
fetchName().finally(() => {
  // No parameters — cleanup only
  console.log("Done!");
});

The Promise<T> generic creates a contract between the function that creates the promise and the code that consumes it. The creator says "I will give you a T" and TypeScript ensures the consumer uses it correctly. When functions return explicit Promise<T> types, they become self-documenting — every developer reading the code immediately knows what async data to expect.

03Creating Typed Promises

There are several ways to create typed promises in TypeScript. Each approach has its own use case and ergonomics.

Method 1: Using the Promise constructor with explicit typing

const wait = new Promise<void>(
  resolve => {
    setTimeout(
      () => resolve(), 1000
    );
  }
);

const data = new Promise<string>(
  (resolve, reject) => {
    const result = fetchData();
    if (result) {
      resolve(result); // string
    } else {
      reject("No data");
      // reject is always unknown
    }
  }
);

Method 2: Using Promise.resolve() with inferred type

const num = Promise.resolve(42);
// Type: Promise<number>
// Inferred from the argument

const user = Promise.resolve({
  name: "Imran",
  age: 25
});
// Type: Promise<{ name: string; age: number }>

Method 3: Typed async functions (the most common!)

async function getData():
  Promise<string[]> {
  return ["chai", "biscuit"];
  // Return type is automatically
  // wrapped in Promise
}

Method 4: Promise.all() and Promise.race()

const p1 = Promise.resolve("hello");
const p2 = Promise.resolve(42);
const p3 = Promise.resolve(true);

const all = Promise.all([
  p1, p2, p3
]);
// Type: Promise<[string, number, boolean]>
// Variadic tuple — preserves order!

const any = Promise.race([p1, p2]);
// Type: Promise<string | number>
// First resolved type wins

const settled = Promise.allSettled([
  p1, p2
]);
// Type:
// Promise<PromiseSettledResult<string | number>[]>

Method 5: Wrapping callback-based APIs

function readFile(
  path: string
): Promise<string> {
  return new Promise(
    (resolve, reject) => {
      fs.readFile(
        path, "utf-8",
        (err, data) => {
          if (err) reject(err);
          else resolve(data);
        }
      );
    }
  );
}

Each of these patterns gives TypeScript the information it needs to track the resolved type through your async operations. The key is to always be explicit about the type parameter when creating promises — rely on inference when it's clear, but annotate when the type might be ambiguous.

04Promise Traps

Typed promises are powerful, but there are common traps that can undermine the type safety they provide. Let's walk through the most frequent mistakes.

Trap 1: The Implicit any Promise

When you don't type your promise, TypeScript might infer Promise<any>, which defeats the purpose entirely. This is especially common with fetch:

// DANGER: Promise<any>
const data = fetch("/api/user")
  .then(res => res.json());

// SAFE: Explicit type
const data: Promise<User> =
  fetch("/api/user")
    .then(res =>
      res.json() as Promise<User>
    );

Trap 2: Double Promise Wrapping

Returning a Promise<Promise<T>> creates confusion. TypeScript allows it, but it's usually a sign you're doing something wrong:

// BAD: Double wrapped
function bad():
  Promise<Promise<string>> {
  return Promise.resolve(
    Promise.resolve("hello")
  );
}

// GOOD: Single level
function good(): Promise<string> {
  return Promise.resolve("hello");
}

Trap 3: Forgetting that .catch receives unknown

Many developers assume .catch(err) gives them an Error object. But TypeScript types it as unknown for good reason — promises can reject with strings, numbers, or even undefined:

Promise.reject("something broke")
  .catch(err => {
    // err is unknown, NOT Error
    // err.message would crash!
    if (typeof err === "string") {
      console.log(err);
    }
  });

Trap 4: async Function Return Type Pitfall

An async function ALWAYS returns a Promise. If you return a non-promise value, it gets wrapped in a promise automatically. But if you return a promise, it doesn't get double-wrapped:

async function example():
  Promise<string> {
  return "hello"; // string
  // Auto-wrapped to Promise<string>
}

async function example2():
  Promise<string> {
  return Promise.resolve("hello");
  // Also works: Promise<string>
  // Not double-wrapped!
}

Understanding these traps helps you write clean, type-safe async code that doesn't crash at runtime.

05Promise Types Cheatsheet

Here's your complete cheatsheet for typing promises in TypeScript. Pin this to your mental board, bhai!

Basic Types:

Promise<T>   // Resolves with T
Promise<void> // Resolves with undefined
Promise<never> // Never resolves (infinite)

// Common patterns
Promise<string>
Promise<User[]>
Promise<{ data: T; error: null }>

Creating Typed Promises:

new Promise<T>((resolve, reject) => {})
Promise.resolve(value) // infers T
async function fn(): Promise<T> {}

Consuming Promises:

.then((val: T) => {})     // T is resolved type
.catch((err: unknown) => {}) // unknown error
.finally(() => {})    // no value

Promise Combinators:

Promise.all([p1, p2])     // [T1, T2]
Promise.race([p1, p2])    // T1 | T2
Promise.allSettled([p1])  // PromiseSettledResult<T>[]

Key Rules:

  • Always type your promises explicitly — Promise<T> is a contract, not an option
  • .catch() receives unknown — narrow it before using
  • Never create Promise<Promise<T>> — unwrap to a single level
  • Async functions always return promises — the return type is automatically wrapped
  • Use Promise.all() for parallel operations — TypeScript preserves tuple types

The Golden Rule: "Promise<T> is like a chai token — it guarantees exactly what you'll get when it's ready. Type the promise, trust the compiler, and async code becomes predictable, bhai!"

Key Takeaways

  • Promise is a generic type where T is the resolved value type — like a chai token guarantee
  • .then() receives exactly T — TypeScript knows the type, no casting needed
  • .catch() receives unknown — always narrow the error type before using it
  • Promise.all() preserves tuple types — [Promise, Promise] becomes [string, number]
  • Async functions automatically wrap return types in Promise — never create Promise>
  • Always type your promises explicitly to avoid the implicit Promise trap
Course Search
Search across all chapters & stages
📖

Search the course

Type any topic — branching, stash, rebase, hooks — and jump straight to that chapter.

merge branchesgit stashundo commitrebase