Function Overloading
Function overloading lets a single function present several distinct call signatures, each with its own parameter and return types. It is the tool to reach for when a function’s return type depends on the shape of its arguments in a way a simple union cannot express. In TypeScript, overloading is purely a compile-time construct: you write one or more overload signatures with no body, followed by a single implementation signature with the body. This page explains the mechanics, the rules the compiler enforces, and when a union or generic is the better choice.
Overload signatures and the implementation signature
Write the public signatures first, each ending in a semicolon with no body. Then write one implementation signature whose body handles every overload. Callers see only the overload signatures — never the implementation.
function parse(value: string): string[];
function parse(value: number): number[];
function parse(value: string | number): string[] | number[] {
return typeof value === "string" ? value.split("") : [value];
}
const chars = parse("hi"); // string[]
const nums = parse(42); // number[]
The call parse("hi") matches the first overload and is typed string[]; parse(42) matches the second and is number[]. A plain string | number parameter could not give the caller this precise, input-dependent return type.
The implementation signature is not callable
The implementation signature exists only to satisfy the body — it is invisible to callers. Even though its parameter type is string | number, you cannot call parse with that union directly unless an overload also permits it.
function parse(value: string): string[];
function parse(value: number): number[];
function parse(value: string | number): string[] | number[] {
return typeof value === "string" ? value.split("") : [value];
}
declare const input: string | number;
// ❌ Error: No overload matches this call.
parse(input);
Because no overload accepts string | number, the union call is rejected. If you want to allow it, add a third overload: function parse(value: string | number): string[] | number[];.
The implementation signature must be compatible
The implementation signature must be broad enough to accept every overload’s parameters and produce every overload’s return type. If it is too narrow, the overloads fail to match it.
// ❌ Error: This overload signature is not compatible with its implementation signature.
function format(value: string): string;
function format(value: number): number;
function format(value: string): string { // implementation too narrow
return value;
}
The fix is to widen the implementation to value: string | number and return string | number. The implementation signature does not need to be type-safe internally beyond this — narrowing inside the body (with typeof) recovers precision.
Order matters: most specific first
TypeScript resolves a call against overloads top to bottom and picks the first match. List more specific signatures before more general ones, or the general one will shadow them.
function getLength(value: string): number;
function getLength(value: unknown[]): number;
function getLength(value: string | unknown[]): number {
return value.length;
}
If a broad overload like (value: any): number appeared first, every call would resolve to it and the specific return types would be lost.
Prefer a union parameter or a generic over overloads when you can. Overloads are verbose, easy to get out of sync with the implementation, and provide no checking between the signatures and the body. Use them only when the relationship between input and output genuinely cannot be expressed otherwise.
Overloads vs. unions vs. generics
| Technique | Best for | Drawback |
|---|---|---|
| Overloads | Return type depends on which input shape | Verbose, implementation unchecked against overloads |
| Union parameter | One return type for several input types | Cannot vary return type per input |
| Generic function | Return type relates to input type parameter | Less explicit for distinct concrete cases |
A generic often replaces overloads more cleanly. For example, function first<T>(arr: T[]): T | undefined handles every element type without listing each one.
Overloads in object types
Call signatures inside an interface or object type can also be overloaded — list multiple call signatures with no implementation. This is how built-in types like Document.createElement model their many element-specific return types.
interface StringFactory {
(length: number): string;
(char: string, count: number): string;
}
See the call signatures page for how these combine with constructable types and hybrid objects.
Best Practices
- Reach for overloads only when the return type depends on the input shape in a way a union cannot capture.
- Place overload signatures above the implementation; the implementation has no callable signature of its own.
- Make the implementation signature broad enough to cover every overload’s parameters and returns.
- List specific overloads before general ones — resolution picks the first match.
- Prefer a generic function or a union parameter when either expresses the relationship just as well.
- Narrow inside the implementation body with
typeof/instanceofto recover precise types. - Keep overload signatures and the implementation in sync; the compiler does not fully cross-check them.