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:
- Overload signatures — one or more declarations that describe valid call patterns (no body)
- Implementation signature — the actual function with a body; its parameter and return types must be wide enough to satisfy all overload signatures
- The implementation — the body that handles all cases, usually with type guards
// 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 overloadReturn 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.
// 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!Overloads with Different Arities
Overloads are also used when a function accepts different numbers of arguments with different semantics.
// 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
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 |
// 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[] — preciseOverloads 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.
// 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 APIT extends string ? string[] : number[]) can also express some overload patterns, but overloads produce better error messages and documentation.Common Overload Pitfalls
Making the implementation signature too narrow — it must cover all overload signatures
Putting the most specific overload last — TypeScript matches overloads top-to-bottom
Forgetting that callers cannot call the implementation signature directly
Overloading when a generic or optional parameter is simpler and just as precise
Having an overload that is identical to the implementation signature (redundant)
// 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)Real-World Example: fetch Wrapper
// 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.
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.
// 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 | nullTesting 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.
// 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");
});
});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.
// 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 possibleWhen 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