TypeScript Function Overloading
A reference for declaring multiple type signatures on a single TypeScript function.
TL;DR
- 01Declare multiple overload signatures before the implementation function.
- 02Implement the function body using union types to handle all signatures.
- 03Callers see only the matching overload, not the implementation signature.
Tips
- 01Use overloading when a function behaves differently based on input types — it's better than union types for callers.
Warnings
- 01Overloads are compile-time only — the implementation still needs to handle all types at runtime.
Basic Function Overloading
- Declare multiple overload signatures before the implementation.
// Overload signatures function add(a: number, b: number): number; function add(a: string, b: string): string; // Implementation function add(a: any, b: any) { return a + b; } add(1, 2); // OK: returns number add('a', 'b'); // OK: returns string - The implementation signature is not callable by consumers.
function format(value: string): string; function format(value: number): string; function format(value: string | number): string { return String(value); } // format(true); // error — implementation signature is hidden - Use at least two overload signatures before the implementation.
// One overload is not useful — use two or more function parse(input: string): number; function parse(input: number): string; function parse(input: string | number): string | number { return typeof input === "string" ? Number(input) : String(input); } - Overloads must be compatible with the implementation signature.
function wrap(value: string): string[]; function wrap(value: number): number[]; function wrap(value: string | number): string[] | number[] { return [value as any]; } - Use overloads to express arity differences explicitly.
function createUser(name: string): User; function createUser(name: string, role: string): User; function createUser(name: string, role?: string): User { return { name, role: role ?? "user" }; }
Optional Parameters
- Use overloads to give different return types per argument count.
function greet(name: string): string; function greet(name: string, age: number): string; function greet(name: string, age?: number) { if (age) { return `Hello, ${name}! You are ${age}.`; } return `Hello, ${name}!`; } greet('Alice'); // OK greet('Bob', 30); // OK - Overloads prevent callers from passing invalid argument combinations.
function connect(host: string): Connection; function connect(host: string, port: number): Connection; function connect(host: string, port?: number): Connection { return new Connection(host, port ?? 80); } - Optional overloads are clearer than one signature with many optional params.
// Hard to reason about — too many optional flags // function render(a: string, b?: string, c?: string, d?: number): void // Better: separate overloads for each valid call pattern function render(template: string): void; function render(template: string, context: Record<string, string>): void; function render(template: string, context?: Record<string, string>): void { // implementation } - Name overload parameters consistently for readability.
function fetch(url: string): Promise<Response>; function fetch(url: string, options: RequestInit): Promise<Response>; function fetch(url: string, options?: RequestInit): Promise<Response> { return window.fetch(url, options); } - Add a default-return overload last to handle the fallback case.
function get(key: "count"): number; function get(key: "label"): string; function get(key: string): unknown; function get(key: string): unknown { return store[key]; }
Different Return Types
- Overloads let callers get the right return type per input type.
function process(input: string): string; function process(input: number): number; function process(input: string | number): string | number { if (typeof input === 'string') { return input.toUpperCase(); } return input * 2; } const str = process('hello'); // string const num = process(5); // number - Without overloads, callers receive a union return type.
// Without overloads — caller must narrow the return function convert(x: string | number): string | number { return typeof x === "string" ? x.toUpperCase() : x * 2; } const result = convert("hi"); // string | number — inconvenient - Map input literal types to different output types with overloads.
function read(format: "json"): object; function read(format: "text"): string; function read(format: "buffer"): ArrayBuffer; function read(format: string): object | string | ArrayBuffer { // implementation chooses based on format return {}; } - Use generics when the return type depends on the input type generically.
// Overload approach function identity(x: string): string; function identity(x: number): number; function identity(x: any): any { return x; } // Generic approach — simpler for symmetric cases function identityG<T>(x: T): T { return x; } - Combine overloads with optional flags for conditional return shapes.
function search(query: string, raw: true): string[]; function search(query: string, raw?: false): SearchResult[]; function search(query: string, raw?: boolean): string[] | SearchResult[] { const results = queryIndex(query); return raw ? results.map(r => r.raw) : results; }
Complex Overloads
- Use overloads for functions that accept multiple input shapes.
function merge(obj1: object, obj2: object): object; function merge(arr1: any[], arr2: any[]): any[]; function merge(item1: any, item2: any) { if (Array.isArray(item1) && Array.isArray(item2)) { return [...item1, ...item2]; } return { ...item1, ...item2 }; } - Add overloads for callback vs non-callback variants of a function.
function load(id: number): Promise<User>; function load(id: number, cb: (user: User) => void): void; function load(id: number, cb?: (user: User) => void): Promise<User> | void { const p = fetchUser(id); if (cb) { p.then(cb); } else { return p; } } - Overload class methods the same way as standalone functions.
class Formatter { format(value: string): string; format(value: number): string; format(value: string | number): string { return String(value).trim(); } } - Order overloads from most specific to least specific.
function normalize(input: string[]): string[]; function normalize(input: string): string; function normalize(input: string | string[]): string | string[] { return Array.isArray(input) ? input.map(s => s.trim()) : input.trim(); } - Overload interface call signatures for typed function objects.
interface Converter { (value: string): number; (value: number): string; } const convert: Converter = (value: any) => typeof value === "string" ? Number(value) : String(value);
Best Practices
- Keep overloads focused so each signature is clearly distinct.
// Good: overloads are distinct function getValue(key: string): string; function getValue(key: number): number; // Avoid: overlapping overloads confuse callers // function getValue(key: string | number): any { } - Avoid overloads when a generic achieves the same result more simply.
// Overloads: verbose for symmetric relationships function echo(x: string): string; function echo(x: number): number; // Generic: cleaner for identity-like functions function echo<T>(x: T): T { return x; } - Document each overload signature with JSDoc for IDE tooltips.
/** Converts input to uppercase */ function transform(input: string): string; /** Doubles a numeric input */ function transform(input: number): number; function transform(input: string | number): string | number { return typeof input === "string" ? input.toUpperCase() : input * 2; } - Test every declared overload in your test suite.
// Verify each overload signature independently const a: string = transform("hello"); // must be string const b: number = transform(5); // must be number - Don't create an overload if a union parameter already expresses the intent.
// Use union when both inputs produce the same output type function printValue(value: string | number): void { console.log(String(value)); }
FAQ
The overload signatures are what TypeScript checks against — if none of them match the argument types you're passing, the call fails at compile time even if your implementation would handle it. Add an overload signature that covers the case, or widen an existing one.
Use overloading when the return type changes based on input type, or when you want callers to get a precise return type without casting. Union parameters are simpler when the behavior and return type are the same regardless of input.
No — the implementation signature is invisible to callers. Only the overload signatures above it are part of the public API, so you must explicitly declare every callable variant before the implementation.
Declare multiple method signatures with the same name inside the class body, then write a single implementation that accepts union types. The syntax is identical to standalone function overloading — no 'function' keyword needed.
Not directly — the overload syntax only works with function declarations and class methods. For arrow functions, define an overloaded call signature on an interface or type alias and then assign a compatible function to a variable of that type.