TypeScript Typing Promises

A Promise represents a value that will be available later — either a successful result or an error. In plain JavaScript, a Promise’s eventual value has no declared type, so you only find out what you got when the code runs. TypeScript fixes this by attaching a type to what a Promise resolves to, using the generic Promise<T> type. This lets the compiler check every .then() callback, every await expression, and every async function’s return value before your code ever runs.

Overview: How TypeScript Types Promises

Promise is a generic interface built into TypeScript’s standard library (declared in lib.es2015.promise.d.ts). Its signature is essentially interface Promise<T>, where T is the type of the value the promise resolves with. A Promise<number> is a promise that will eventually produce a number; a Promise<void> is a promise that resolves with no meaningful value at all.

The type parameter flows through every place the promise is consumed. When you write promise.then(value => ...), TypeScript infers the type of value from the promise’s T. When you await a promise, the expression’s type is T — the “wrapper” is unwrapped by the compiler, not by any runtime magic specific to types. This is a key point: types exist only during compilation. At runtime, a Promise<string> is a completely ordinary JavaScript Promise object; there is no type information attached to it once the code is compiled to JavaScript. The compiler’s job is to prove, ahead of time, that everything you do with that promise’s value is consistent with the type you declared.

async functions always return a Promise

Whenever you mark a function async, TypeScript automatically wraps its return type in a Promise. If the body has return 42;, the function’s type is inferred as Promise<number>, not number — even though you write a plain return statement. If the body throws, the rejection is untyped by default (TypeScript does not track a separate “error type” for rejections, similar to how JavaScript’s catch can throw anything).

Syntax

// Explicit Promise return type on a function that builds its own promise
function name(param: ParamType): Promise<ResultType> {
  return new Promise((resolve, reject) => {
    // resolve(value) on success
    // reject(error) on failure
  });
}

// Explicit Promise return type on an async function
async function name(param: ParamType): Promise<ResultType> {
  const value = await somethingElse();
  return value;
}
Piece Meaning
Promise<ResultType> The declared or inferred return type; ResultType is what the promise resolves to.
resolve(value) Fulfills the promise; value must match ResultType.
reject(error) Rejects the promise; TypeScript does not constrain the type of error.
await expr Suspends the async function and evaluates to the resolved T of expr‘s Promise<T>.
Promise<void> Used when a promise resolves but carries no useful value.

Examples

Example 1: A function that returns a typed Promise

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

delay(100).then((value) => {
  console.log(`Waited ${value}ms`);
});

Output:

Waited 100ms

The Promise constructor is generic, but TypeScript infers its type parameter from how resolve is called inside the executor — here, resolve(ms) where ms is a number, so the executor’s inferred type is Promise<number>, matching the declared return type. In the .then() callback, value is automatically typed as number with no annotation needed.

Example 2: async/await with a typed result and error handling

interface User {
  id: number;
  name: string;
}

async function fetchUser(id: number): Promise<User> {
  if (id <= 0) {
    throw new Error("Invalid id");
  }
  return { id, name: "Ada Lovelace" };
}

async function run(): Promise<void> {
  try {
    const user = await fetchUser(1);
    console.log(`${user.id}: ${user.name}`);
  } catch (err) {
    if (err instanceof Error) {
      console.log(`Failed: ${err.message}`);
    }
  }
}

run();

Output:

1: Ada Lovelace

Because fetchUser is declared to return Promise<User>, TypeScript requires every return statement in its body to produce a value assignable to User (or to throw). After await fetchUser(1), the variable user is typed as User, so user.id and user.name are checked against that interface — a typo like user.naem would be caught at compile time.

Example 3: Promise.all with inferred tuple types

function getName(): Promise<string> {
  return Promise.resolve("Grace");
}

function getAge(): Promise<number> {
  return Promise.resolve(33);
}

async function combine(): Promise<void> {
  const [name, age] = await Promise.all([getName(), getAge()]);
  console.log(`${name} is ${age}`);
}

combine();

Output:

Grace is 33

Promise.all is typed so that it takes an array (or tuple) of promises and returns a single promise of a tuple with the corresponding resolved types in order: here Promise<[string, number]>. Destructuring the awaited result gives name: string and age: number with no manual annotation — the compiler tracks each position independently.

Example 4: A generic helper that wraps any value in a Promise

function wrapValue<T>(value: T): Promise<T> {
  return new Promise((resolve) => {
    setTimeout(() => resolve(value), 10);
  });
}

async function showWrapped(): Promise<void> {
  const message = await wrapValue<string>("hello generics");
  console.log(message);
}

showWrapped();

Output:

hello generics

Combining generics with Promise<T> lets you write one reusable function that stays fully typed for any input. Calling wrapValue<string>("hello generics") fixes T to string, so the returned promise is Promise<string> and message is inferred as string after await. This is exactly the pattern used by typed wrappers around fetch or database clients: a generic function returning Promise<T> lets each call site choose its own result type.

Under the Hood

When the compiler checks an async function, it looks at every return statement, infers (or checks against the declared type) what those values resolve to, and wraps the result in Promise<T> for the function’s overall type. When it checks an await expression, it does the reverse: it looks at the operand’s type, and if that type is Promise<T> (or more generally, anything “thenable” with a matching then method), the expression’s type becomes T. Nested promises are flattened automatically — awaiting a Promise<Promise<string>> still gives you a plain string, matching how Promises behave at runtime.

None of this exists once your code is compiled. The emitted JavaScript for an async function fetchUser(id) { ... } has no trace of Promise<User> anywhere — it is ordinary async/await syntax (or, for older targets, generator-based helper code). The type checking happens once, at build time; if it passes, you can trust that the shapes line up, but the runtime behavior of Promises is exactly the same as in plain JavaScript.

Common Mistakes

Mistake 1: Declaring an async function’s return type without Promise

async function getValue(): number {
  return 42;
}

This fails to compile with an error similar to “The return type of an async function or method must be the global Promise<T> type.” Every async function’s declared return type must itself be a Promise, because that is what calling it actually produces at runtime.

async function getValue(): Promise<number> {
  return 42;
}

getValue().then((value) => console.log(value));

Output:

42

Mistake 2: Typing a catch clause variable as a specific type

async function risky(): Promise<void> {
  throw new Error("boom");
}

async function run(): Promise<void> {
  try {
    await risky();
  } catch (err: Error) {
    console.log(err.message);
  }
}

Under --strict, this produces “Catch clause variable type annotation must be ‘any’ or ‘unknown’ if specified.” Because JavaScript lets you throw any value at all (a string, a plain object, anything), TypeScript refuses to let you assume a caught value is a specific class like Error — you must narrow it yourself.

async function risky(): Promise<void> {
  throw new Error("boom");
}

async function run(): Promise<void> {
  try {
    await risky();
  } catch (err: unknown) {
    if (err instanceof Error) {
      console.log(err.message);
    }
  }
}

run();

Output:

boom

Best Practices

  • Always let TypeScript infer Promise<T> from your return statements when possible, but write an explicit return type on exported/public async functions so the contract is documented and stable.
  • Use Promise<void> for async functions that perform side effects and don’t produce a meaningful value, rather than Promise<undefined> or omitting the type.
  • Type catch variables as unknown (the strict-mode default) and narrow with instanceof Error before accessing .message.
  • Prefer Promise.all for independent async operations you want to run concurrently — TypeScript preserves each result’s individual type in the returned tuple.
  • Avoid wrapping already-async values in new Promise(...) unnecessarily (the “Promise constructor antipattern”) — call and return/await the existing promise instead.
  • When writing a generic async helper, parameterize it with <T> and return Promise<T> so every call site keeps its own precise type instead of collapsing to any.

Practice Exercises

  • Write a function fetchScore(playerId: number): Promise<number> that resolves after a short setTimeout with a fixed score value, then call it with .then() and log the result.
  • Write an async function loadProfile(id: number): Promise<{ id: number; email: string }> that throws an Error when id is negative, and a caller that awaits it inside a try/catch, narrowing the caught value with instanceof Error.
  • Write two functions returning Promise<string> and Promise<boolean> respectively, then use Promise.all to await both together and log a message that uses both resolved values.

Summary

  • Promise<T> is a generic type describing a promise that resolves with a value of type T.
  • Marking a function async makes TypeScript automatically wrap its return type in Promise<...>.
  • await unwraps a Promise<T> down to T at the type level, mirroring what happens at runtime.
  • Promise.all preserves each promise’s individual resolved type in the resulting tuple.
  • Types are erased at compile time — the emitted JavaScript has no Promise<T> information, only ordinary Promise objects.
  • Catch clause variables are unknown under --strict; always narrow with instanceof before using error-specific properties.