Technology · TypeScript
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.
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)); }
Tip: Use overloading when a function behaves differently based on input types — it's better than union types for callers.
Warning: Overloads are compile-time only — the implementation still needs to handle all types at runtime.