TypeScriptReturn Types

Return Types

TypeScript can infer return types from the function body, so why bother annotating them explicitly? Because explicit annotations act as a contract — the compiler verifies that every code path returns something compatible, not just that it infers a type. This section covers every return type you will encounter, from the simple to the advanced.

Explicit Return Type Annotations

Place the return type annotation after the closing parenthesis, before the body:

TS
// Without annotation — TypeScript infers string | number
function format(value: string | number) {
  if (typeof value === 'string') return value.trim();
  return value.toFixed(2);
}

// With annotation — you declare the intent, TS verifies it
function format2(value: string | number): string {
  if (typeof value === 'string') return value.trim();
  return value.toFixed(2); // toFixed returns string — OK
}

// TS catches mistakes immediately:
function broken(value: string | number): string {
  if (typeof value === 'string') return value.trim();
  return value; // TS2322: number is not assignable to string
}

Why annotate when TypeScript can infer?

  1. Documents intent — readers see the contract without reading the body

  2. Catches bugs at the definition, not at every call site

  3. Prevents return type from accidentally widening (e.g. string | undefined instead of string)

  4. Required for overloaded functions and some recursive functions

void — Function Returns Nothing Useful

void is the return type for functions whose return value should not be used. It is distinct from undefined:

TS
// void: callers should not use the return value
function logMessage(msg: string): void {
  console.log(msg);
  // implicitly returns undefined, but typed as void
}

// void allows returning a value (for callback compatibility)
type Callback = () => void;
const cb: Callback = () => 42; // OK — the 42 is discarded

// undefined: must explicitly return undefined
function mustReturnUndefined(): undefined {
  return undefined; // required
  // return 42;     // TS2322 error
}

const result = logMessage('hi'); // result is void — don't use it
Tip
Use void for event handlers, callbacks, and fire-and-forget functions. Use undefined only when you truly want to enforce that nothing is returned.
never — Function Never Returns

never is the return type of a function that cannot complete normally — it either always throws an error or enters an infinite loop. TypeScript uses never for exhaustiveness checking:

TS
// Always throws — never returns
function invariant(condition: boolean, message: string): asserts condition {
  if (!condition) throw new Error(`Invariant failed: ${message}`);
}

function fail(message: string): never {
  throw new Error(message);
}

// Infinite loop — also never
function runForever(): never {
  while (true) {
    // server event loop, etc.
  }
}

// Exhaustiveness guard
type Direction = 'north' | 'south' | 'east' | 'west';

function getVector(dir: Direction): [number, number] {
  switch (dir) {
    case 'north': return [0, 1];
    case 'south': return [0, -1];
    case 'east':  return [1, 0];
    case 'west':  return [-1, 0];
    default: return fail(`Unknown direction: ${dir}`);
    // If you add 'up' to Direction, TS will still find a path
    // to the default — consider assertNever for stricter exhaustion
  }
}

Return type

Meaning

Assignable to everything?

never

Function never returns — throws or loops

Yes — never is a subtype of all types

void

Return value should not be used

No — void is not a value type

undefined

Must return undefined

No — only assignable to undefined/unknown

undefined vs void in Practice

TS
// void in a callback type allows any return — very permissive
const forEach: (fn: () => void) => void = (fn) => fn();

// This is intentional: Array.prototype.forEach uses () => void
// so you can pass a function that returns a value without error
['a', 'b', 'c'].forEach((s) => s.length); // OK — return value ignored

// When strict undefined checking matters:
type MaybeTransform = (value: string) => string | undefined;

function tryTransform(
  value: string,
  transform: MaybeTransform
): string {
  const result = transform(value);
  return result ?? value; // handle undefined explicitly
}
Returning Union Types

A function can return one of several types. The caller must narrow the return value before using type-specific members:

TS
type Success<T> = { ok: true; value: T };
type Failure     = { ok: false; error: string };
type Result<T>   = Success<T> | Failure;

function parseJSON<T>(raw: string): Result<T> {
  try {
    return { ok: true, value: JSON.parse(raw) as T };
  } catch (e) {
    return { ok: false, error: (e as Error).message };
  }
}

const result = parseJSON<{ name: string }>('{"name":"Alice"}');

// Caller must narrow
if (result.ok) {
  console.log(result.value.name); // string
} else {
  console.error(result.error);    // string
}
Promise Return Types for Async Functions

Annotate async functions with Promise<T> where T is the resolved value. TypeScript automatically wraps the return in a Promise, so you never need to annotate the actual return statement — only the function signature:

TS
interface User {
  id: string;
  name: string;
  email: string;
}

// Annotate with Promise<User>
async function fetchUser(id: string): Promise<User> {
  const res = await fetch(`/api/users/${id}`);
  if (!res.ok) throw new Error(`HTTP ${res.status}`);
  return res.json(); // TypeScript trusts Promise<User> here
}

// For functions that can fail gracefully, use a Result type
async function safeFetchUser(id: string): Promise<Result<User>> {
  try {
    const user = await fetchUser(id);
    return { ok: true, value: user };
  } catch (e) {
    return { ok: false, error: (e as Error).message };
  }
}
Note
TypeScript infers Promise<T> automatically for async functions. Explicit annotations help when the inferred type is wider than intended, or when the function has multiple return paths that could diverge.
ReturnType Utility Type

ReturnType<T> extracts the return type of a function type as a type-level value. This is invaluable when wrapping third-party functions:

TS
function createStore(initial: number) {
  let value = initial;
  return {
    get:       () => value,
    set:       (n: number) => { value = n; },
    increment: () => ++value,
  };
}

// Extract what createStore returns — without duplicating the type
type Store = ReturnType<typeof createStore>;
// → { get: () => number; set: (n: number) => void; increment: () => number }

function withLogging(store: Store): Store {
  return {
    get:       () => { console.log('get'); return store.get(); },
    set:       (n) => { console.log(`set(${n})`); store.set(n); },
    increment: () => { console.log('increment'); return store.increment(); },
  };
}

TS
// Awaited + ReturnType for async functions
async function getConfig() {
  return { host: 'localhost', port: 5432 };
}

type Config = Awaited<ReturnType<typeof getConfig>>;
// → { host: string; port: number }
Overloads for Narrowed Return Types

Sometimes the return type depends on the argument types. Use overloads to tell TypeScript about this relationship:

TS
function parse(input: string): string[];
function parse(input: number): number[];
function parse(input: string | number): string[] | number[] {
  if (typeof input === 'string') return input.split(',');
  return [input];
}

const strings = parse('a,b,c'); // string[]
const numbers = parse(42);      // number[]
// No union in the result — TypeScript picks the right overload
Generator Function Return Types

Generator functions return a Generator<Yield, Return, Next> type:

  • Yield — the type of values produced by yield
  • Return — the type of the final return value
  • Next — the type passed to generator.next(value)

TS
// A simple infinite counter generator
function* counter(start = 0): Generator<number, void, unknown> {
  let n = start;
  while (true) {
    yield n++;
  }
}

const gen = counter(10);
console.log(gen.next().value); // 10
console.log(gen.next().value); // 11
console.log(gen.next().value); // 12

TS
// Async generator — yields values asynchronously
async function* paginate<T>(
  fetcher: (page: number) => Promise<T[]>,
  maxPages = Infinity
): AsyncGenerator<T, void, unknown> {
  let page = 1;
  while (page <= maxPages) {
    const items = await fetcher(page);
    if (items.length === 0) return;
    for (const item of items) yield item;
    page++;
  }
}

// Consume with for-await-of
for await (const user of paginate((p) => fetchPage(p), 5)) {
  console.log(user);
}
When to Annotate vs When to Infer

TypeScript's inference is powerful — you do not need to annotate everything. Here is a practical guide:

Situation

Recommendation

Simple utility functions

Let TypeScript infer

Public API / exported functions

Always annotate — documents the contract

Function with multiple return paths

Annotate to catch diverging paths early

Recursive functions

Must annotate — inference may fail

Async functions returning complex types

Annotate with Promise<T>

Functions with overloads

Annotation required for each overload

Private helpers

Infer unless the body is long or complex

Success
Explicit return type annotations are a form of test — the compiler checks every code path for you. They are most valuable on public APIs, complex functions, and any function where inference would produce a wider type than you intend.
Common Mistakes
  • Confusing void and undefined — void allows any return, undefined enforces nothing

  • Returning never when void is intended — never means unreachable, not "no value"

  • Forgetting Awaited<> when extracting the return type of an async function

  • Not annotating exported functions — callers see inferred types that may change

  • Using any as a return type shortcut — defeats the purpose of TypeScript

Quick Reference
  • : T — explicit return type annotation after the closing parenthesis

  • void — return value ignored; compatible with any return

  • undefined — must return undefined only

  • never — function always throws or loops forever

  • Promise<T> — async function resolving to T

  • ReturnType<typeof fn> — extract return type from a function

  • Awaited<ReturnType<typeof asyncFn>> — unwrap async return type

  • Generator<Y, R, N> — generator with yield, return, and next types