TypeScriptFunction Overloads

Function Overloads

Function overloads let you define multiple call signatures for the same function. When callers see different parameter types or counts, TypeScript picks the right signature and infers the correct return type for each case. This gives you precise, context-sensitive type checking on a single function name.

The Anatomy of an Overload

A TypeScript function overload has three parts:

  1. Overload signatures — one or more declarations that describe valid call patterns (no body)
  2. Implementation signature — the actual function with a body; its parameter and return types must be wide enough to satisfy all overload signatures
  3. The implementation — the body that handles all cases, usually with type guards

TS
// 1. Overload signatures (no body)
function format(value: number): string;
function format(value: string): string;

// 2. Implementation signature (must cover both above)
function format(value: number | string): string {
  // 3. Implementation body — handles all cases
  if (typeof value === "number") {
    return value.toFixed(2);
  }
  return value.trim();
}

// Callers only see the overload signatures:
format(42);       // string  (uses first signature)
format("hello "); // string  (uses second signature)
// format(true);  // Error — boolean is not in any overload
Note
The implementation signature is NOT visible to callers. Only the overload signatures are. The implementation signature must be compatible with all overload signatures, but it is an implementation detail, not part of the public API.
Return Type Variation

One of the most powerful uses of overloads is varying the return type based on input type — something a simple union return type cannot express cleanly.

TS
// Without overloads: return type is always string | number
function parseValue(input: string | number): string | number {
  return typeof input === "string" ? parseInt(input) : input.toString();
}
const r1 = parseValue("42");  // string | number — not precise enough!
const r2 = parseValue(42);    // string | number — same issue

// With overloads: return type matches the input type
function parseValue(input: string): number;
function parseValue(input: number): string;
function parseValue(input: string | number): string | number {
  return typeof input === "string" ? parseInt(input) : input.toString();
}

const r3 = parseValue("42"); // number  — precise!
const r4 = parseValue(42);   // string  — precise!
Success
Overloads let the return type be a function of the input type — something generics can also do, but overloads express more explicitly for a small number of known variants.
Overloads with Different Arities

Overloads are also used when a function accepts different numbers of arguments with different semantics.

TS
// createElement with 1, 2, or 3 arguments
function createElement(tag: string): HTMLElement;
function createElement(tag: string, text: string): HTMLElement;
function createElement(tag: string, text: string, className: string): HTMLElement;
function createElement(
  tag: string,
  text?: string,
  className?: string,
): HTMLElement {
  const el = document.createElement(tag);
  if (text)      el.textContent = text;
  if (className) el.className   = className;
  return el;
}

const div     = createElement("div");
const heading = createElement("h1", "Hello, World!");
const styled  = createElement("p", "Paragraph", "highlight");
Overloads on Object Methods

TS
class EventEmitter<Events extends Record<string, unknown>> {
  private listeners: Map<string, Function[]> = new Map();

  // Overload signatures for on()
  on<K extends keyof Events>(event: K, listener: (data: Events[K]) => void): this;
  on(event: string, listener: Function): this;

  // Implementation
  on(event: string, listener: Function): this {
    const existing = this.listeners.get(event) ?? [];
    this.listeners.set(event, [...existing, listener]);
    return this;
  }

  emit<K extends keyof Events>(event: K, data: Events[K]): void;
  emit(event: string, data: unknown): void;
  emit(event: string, data: unknown): void {
    this.listeners.get(event)?.forEach((fn) => fn(data));
  }
}

type AppEvents = {
  login:  { userId: string; timestamp: number };
  logout: { userId: string };
};

const bus = new EventEmitter<AppEvents>();

bus.on("login",  (data) => console.log(data.userId));   // data: { userId: string; timestamp: number }
bus.on("logout", (data) => console.log(data.userId));   // data: { userId: string }
Overloads vs. Union Parameters

Union parameters and overloads can look similar but have important differences. Use overloads when the return type or behavior depends on which variant the caller passes.

Feature

Union param

Overloads

Return type varies by input

No — always a union

Yes — each overload can return a different type

Error messages

Mentions the union

Mentions the specific overload

Autocomplete

Shows union members

Shows each signature separately

Complexity

Simpler

More declarations

Good for N variants

Unlimited

Works best for 2–4 well-known variants

TS
// Union parameter — fine when return type does not vary
function log(value: string | number | boolean): void {
  console.log(value);
}

// Overloads — necessary when return type must match input type
function wrapInArray(value: string): string[];
function wrapInArray(value: number): number[];
function wrapInArray(value: string | number): string[] | number[] {
  return [value as any];
}

const strings = wrapInArray("hello"); // string[]  — precise
const numbers = wrapInArray(42);      // number[]  — precise
Overloads and Generics — When to Choose Which

Generics and overloads are sometimes interchangeable. Generics are usually the better choice when the relationship between input and output types is purely structural (the output shape mirrors the input shape). Use overloads when the return type is a fixed set of distinct types.

TS
// Generic version — works for any T
function identity<T>(value: T): T {
  return value;
}

// Overloaded version — only needed if each case differs significantly
function process(value: string): string[];
function process(value: number): number[];
function process(value: string | number): string[] | number[] {
  if (typeof value === "string") return value.split("");
  return Array.from({ length: value }, (_, i) => i);
}

// Rule of thumb:
// - If input and output are the same shape → use generics
// - If output type depends on which specific input type → use overloads
// - If you have 5+ variants → consider redesigning the API
Tip
Generic conditional types (T extends string ? string[] : number[]) can also express some overload patterns, but overloads produce better error messages and documentation.
Common Overload Pitfalls
  1. Making the implementation signature too narrow — it must cover all overload signatures

  2. Putting the most specific overload last — TypeScript matches overloads top-to-bottom

  3. Forgetting that callers cannot call the implementation signature directly

  4. Overloading when a generic or optional parameter is simpler and just as precise

  5. Having an overload that is identical to the implementation signature (redundant)

TS
// Pitfall 1: implementation too narrow
function bad(x: string): string;
function bad(x: number): number;
function bad(x: string): string { // Error! Must accept string | number
  return x;
}

// Fix: implementation accepts the union
function good(x: string): string;
function good(x: number): number;
function good(x: string | number): string | number {
  return x;
}

// Pitfall 2: wrong order — more specific overloads first
function pickFirst(value: "a"): "first";
function pickFirst(value: string): "other";
function pickFirst(value: string): "first" | "other" {
  return value === "a" ? "first" : "other";
}
// pickFirst("a") → "first"   ✓  (first overload matched)
Warning
TypeScript matches overloads in the order they are declared — the first matching signature wins. Always put the most specific (narrowest) overloads before more general ones.
Real-World Example: fetch Wrapper

TS
// A fetch wrapper that returns different types based on a 'format' flag
function apiFetch(url: string, format: "json"): Promise<unknown>;
function apiFetch(url: string, format: "text"): Promise<string>;
function apiFetch(url: string, format: "blob"): Promise<Blob>;
function apiFetch(
  url: string,
  format: "json" | "text" | "blob",
): Promise<unknown> {
  return fetch(url).then((res) => {
    if (format === "json") return res.json();
    if (format === "text") return res.text();
    return res.blob();
  });
}

// Each call site gets the exact return type:
const data  = await apiFetch("/api/data",  "json"); // Promise<unknown>
const text  = await apiFetch("/api/readme","text"); // Promise<string>
const image = await apiFetch("/api/logo",  "blob"); // Promise<Blob>
Overloading Constructor Signatures

Classes in TypeScript can only have one constructor body, but you can declare multiple constructor overloads to accept different argument shapes.

TS
class Point {
  x: number;
  y: number;

  // Overload signatures
  constructor(x: number, y: number);
  constructor(coords: { x: number; y: number });
  constructor(x: string); // "10,20" format

  // Implementation
  constructor(
    xOrCoords: number | { x: number; y: number } | string,
    y?: number,
  ) {
    if (typeof xOrCoords === "string") {
      const [px, py] = xOrCoords.split(",").map(Number);
      this.x = px;
      this.y = py;
    } else if (typeof xOrCoords === "object") {
      this.x = xOrCoords.x;
      this.y = xOrCoords.y;
    } else {
      this.x = xOrCoords;
      this.y = y!;
    }
  }
}

const p1 = new Point(10, 20);
const p2 = new Point({ x: 10, y: 20 });
const p3 = new Point("10,20");
Overloads in Declaration Files

When writing .d.ts declaration files for JavaScript libraries, overloads let you describe complex APIs accurately without implementation code.

TS
// Excerpt from a hypothetical library declaration file
declare function $<K extends keyof HTMLElementTagNameMap>(
  selector: K,
): HTMLElementTagNameMap[K] | null;
declare function $(selector: string): Element | null;
declare function $(selector: string | keyof HTMLElementTagNameMap): Element | null;

// Consumers get precise return types:
const canvas = $("canvas"); // HTMLCanvasElement | null
const generic = $(".my-class"); // Element | null
Tip
Many popular JavaScript libraries (jQuery, D3, lodash) have extensive overloads in their TypeScript declaration files on DefinitelyTyped. Reading these files is an excellent way to study real-world overload usage.
Testing Overloaded Functions

Because overloads produce different return types, your tests should cover each signature independently to verify both the runtime behavior and the inferred types.

TS
// Function under test
function parseValue(input: string): number;
function parseValue(input: number): string;
function parseValue(input: string | number): string | number {
  return typeof input === "string" ? parseInt(input, 10) : input.toString();
}

// Tests cover each overload signature
describe("parseValue", () => {
  it("converts string to number", () => {
    const result = parseValue("42");
    // TypeScript knows result is 'number' here
    expect(result).toBe(42);
    expect(typeof result).toBe("number");
  });

  it("converts number to string", () => {
    const result = parseValue(42);
    // TypeScript knows result is 'string' here
    expect(result).toBe("42");
    expect(typeof result).toBe("string");
  });
});
Tip
Use TypeScript type assertion helpers in tests (e.g., expectTypeOf from vitest) to verify that overloads return the correct type for each signature — not just the correct runtime value.
Overloads vs. Conditional Return Types

TypeScript's conditional types can sometimes replace overloads with a single signature. The trade-off is readability and error message quality.

TS
// Overload approach — clearer error messages, explicit signatures
function convert(value: string): number;
function convert(value: number): string;
function convert(value: string | number): string | number {
  return typeof value === "string" ? Number(value) : String(value);
}

// Conditional type approach — single signature
type Convert<T extends string | number> = T extends string ? number : string;

function convert2<T extends string | number>(value: T): Convert<T> {
  return (typeof value === "string" ? Number(value) : String(value)) as Convert<T>;
}

// Both produce identical call-site behavior:
const n1 = convert("42");   // number
const s1 = convert(42);     // string
const n2 = convert2("42");  // number
const s2 = convert2(42);    // string

// When to use which:
// - Overloads: small number of well-defined cases, cleaner error messages
// - Conditional types: the return type follows a pattern, more variants possible
When NOT to Use Overloads
  • When a single generic function handles all cases — overloads add noise without benefit

  • When you have more than 4 or 5 overloads — consider a discriminated union parameter or a builder pattern

  • When the parameter types are the same and only names differ — use optional parameters instead

  • When the body is identical for all overloads — a single signature with union params is cleaner

  • When callers always pass the same type — a concrete signature is simpler than an overload

Summary
  • Overloads define multiple call signatures for one function, each with potentially different parameter and return types

  • The implementation signature must accept a superset of all overload parameters

  • Callers see only the overload signatures, not the implementation signature

  • TypeScript matches overloads top-to-bottom — put specific cases first

  • Prefer generics when the relationship between input and output is structural

  • Use overloads when the return type is a fixed set of distinct types based on input

  • The implementation body handles all cases and usually contains type guards

  • Constructor overloads allow multiple initialization patterns for a class

  • Declaration files use overloads extensively to type dynamic JavaScript APIs

  • Test each overload signature independently to verify runtime behavior and inferred types

  • Conditional return types can replace overloads when the pattern is purely structural