Function overloads in TypeScript
Function overloads in TypeScript let you declare multiple signatures for a single function, where the compiler picks the matching one at the call site based on the arguments you pass.
Theory
TL;DR
- Think of it like a menu with two options for the same dish: pass one argument and get one return type; pass two and get another. TypeScript selects automatically.
- Main difference from optional params: each overload enforces exact types for its call pattern, no
anyfallback at the call site. - The implementation signature is hidden from callers. They only see the overload signatures.
- Order matters: specific signatures go first, broad ones last.
- Zero runtime cost - overloads erase to a single JS function after compilation.
Quick example
function greet(name: string): string; // Overload 1
function greet(first: string, last: string): string; // Overload 2
function greet(first: string, last?: string): string { // Implementation (not visible to callers)
return last ? `Hello, ${first} ${last}!` : `Hello, ${first}!`;
}
greet("Alice"); // string - picks overload 1
greet("Bob", "Lee"); // string - picks overload 2
// greet(42); // Error: No overload matches this callThe two overload signatures define what callers can do. The implementation below them handles both cases with an optional parameter. TypeScript never exposes the implementation signature in autocomplete or error messages.
Key difference
Overloads give you a precise return type per argument shape at compile time. A single implementation with union types forces callers to handle string | number as the return even when they pass a specific literal argument. With overloads, passing 'name' to getValue returns string; passing 'age' returns number. No extra type guards needed on the calling side.
When to use
- Return type varies by argument value: overloads (e.g.,
getValue('name')returnsstring,getValue('age')returnsnumber) - Argument count changes the shape of the result: overloads over optionals
- You are writing a library or public API where callers need exact types
- One function handles two clearly different call patterns (GET with no body vs POST with body)
- All return types are the same and args are additive: union with optionals is simpler and cleaner
How TypeScript resolves overloads
The compiler scans overload signatures top-to-bottom and assigns the first match's return type to the call. That happens at compile time, not runtime. No type information survives into the emitted JS. In VS Code or any LSP-based editor, hovering a call site shows the resolved overload, not the implementation signature.
Common mistakes
Putting the broad signature first
// Wrong: compiler picks the first match - specific ones become unreachable
function get(key: string): string | number;
function get(key: 'age'): number; // Never reached
// Correct
function get(key: 'age'): number;
function get(key: string): string | number;
function get(key: string): string | number { /* ... */ }If string comes before 'age', every call matches the broad signature and the literal overload is dead code. This is the most common overload bug on Stack Overflow.
Assuming overloads add runtime safety
function safeDiv(a: number, b: number): number;
function safeDiv(a: number): number;
function safeDiv(a: number, b?: number): number {
return a / b; // b can be undefined at runtime - result is NaN
}
safeDiv(10); // TypeScript is fine, runtime returns NaNOverloads are compile-time only. Guard inside the implementation body.
Leaking optional params into overload signatures
// Compiles, but the second overload misleads callers
function greet(name: string): string;
function greet(first: string, last?: string): string { /* ... */ } // optional in overloadOverload signatures should use required params only. Optional params belong in the implementation.
Overloading when a union is enough
// Return type is always string - overloads add nothing here
function format(value: string): string;
function format(value: number): string;
function format(value: string | number): string { return String(value); }
// Just write:
function format(value: string | number): string { return String(value); }Real-world usage
- React:
React.createElementoverloads by tag type to return different JSX shapes - Node.js:
fs.readFileoverloads callback vs Promise variants - Express:
res.json()overloads object vs string input - Lodash:
_.get()overloads path asstring | string[] | number - React hooks:
useState<string>('')resolves to[string, Dispatch<SetStateAction<string>>]through generic overload matching
From experience: the real value of overloads is often IDE ergonomics. Hovering a call site and seeing exactly Promise<string> instead of Promise<string | number> makes intent clear to anyone reading the code months later.
Follow-up questions
Q: How does overload resolution handle ambiguous calls?
A: The compiler picks the first matching signature in declaration order. If no overload matches, it is a compile error. There is no "best match" search - it stops at the first fit.
Q: What is the difference between overloads and intersection types for function parameters?
A: Overloads branch on argument shape: different args get different return types. Intersection types combine requirements: both constraints must be satisfied simultaneously. Use overloads when the function behaves differently depending on inputs.
Q: Can overloads be generic?
A: Yes. function id<T>(x: T): T; is a valid overload signature. Generic overloads are common in React hooks: useState<string>('') resolves through a generic overload to give you the exact [string, Dispatch<SetStateAction<string>>] tuple.
Q: Do overloads affect bundle size or runtime performance?
A: No. TypeScript erases all overload signatures during compilation. The emitted JS contains only the implementation function.
Q: (Senior) A caller passes a union type as the argument. Which overload does TypeScript pick?
A: TypeScript does not automatically split a union into separate overload lookups. If you pass key: string | 'age', the compiler checks that union as a whole against each signature and most likely resolves to the broad overload's return type. This is a common source of confusion when consuming overloaded APIs with derived types.
Examples
HTTP request wrapper
Typical pattern in Express apps and API clients: typed handlers where GET takes no body and POST requires one.
function request(method: 'GET', url: string): Promise<string>;
function request(method: 'POST', url: string, body: object): Promise<number>;
function request(method: string, url: string, body?: object): Promise<string | number> {
if (method === 'GET') return Promise.resolve(`Fetched ${url}`);
return Promise.resolve(Object.keys(body!).length);
}
const response = request('GET', '/users'); // Promise<string>
const status = request('POST', '/users', {id: 1}); // Promise<number>
// request('POST', '/users'); // Error: body is required for POSTTypeScript forces you to provide body on POST calls and blocks it on GET calls. The implementation handles both with an optional param, but callers never see that detail.
Dynamic return type based on key
A common interview pattern: a function that returns different types depending on a literal string argument.
function getValue(key: 'name'): string;
function getValue(key: 'age'): number;
function getValue(key: 'active'): boolean;
function getValue(key: string): string | number | boolean {
const data = { name: 'Alice', age: 30, active: true };
return data[key as keyof typeof data];
}
const name = getValue('name'); // string
const age = getValue('age'); // number
const active = getValue('active'); // booleanWithout overloads, all three calls return string | number | boolean and you end up writing type guards everywhere the result is used.
Array extraction with optional count
A generic overload showing how argument count changes the return shape.
function getFirst<T>(arr: T[]): T | undefined;
function getFirst<T>(arr: T[], count: number): T[];
function getFirst<T>(arr: T[], count?: number): T | T[] | undefined {
return count === undefined ? arr[0] : arr.slice(0, count);
}
const users = ['Alice', 'Bob', 'Carol'];
const one = getFirst(users); // string | undefined
const three = getFirst(users, 2); // string[]The two overloads say: without a count you might get nothing (empty array case), with a count you always get an array. A single signature with count?: number would return string | string[] | undefined for both calls, which forces unnecessary checks on the caller side.
Short Answer
Interview readyA concise answer to help you respond confidently on this topic during an interview.