Function Overloads in TypeScript
Function overloads let a single function accept different argument counts and types. Learn overload signatures, the implementation signature, and when to use a union instead.
TypeScript function overloads describe multiple distinct ways a function can be called. You write one or more overload signatures above the function, then provide a single implementation that handles all of them. Overloads are useful when different argument counts produce different return types, which a simple union cannot express.
TypeScript tries each overload signature in order. The first one that matches the arguments is used. If none match, the call is rejected. The implementation signature is never tried.
Overload Signatures and the Implementation
An overloaded function has two parts: the overload signatures that callers see, and the implementation signature that only the function body uses. Here is the classic example:
// Overload signatures: what callers can use
function makeDate(timestamp: number): Date;
function makeDate(year: number, month: number, day: number): Date;
// Implementation signature: handles all cases internally
function makeDate(timestampOrYear: number, month?: number, day?: number): Date {
if (month !== undefined && day !== undefined) {
return new Date(timestampOrYear, month, day);
}
return new Date(timestampOrYear);
}Calling this function with either one argument or three arguments matches one of the two overload signatures, and both calls return a valid Date:
const fromTimestamp = makeDate(1700000000000);
const fromParts = makeDate(2026, 7, 17);
console.log(fromTimestamp.getFullYear());
console.log(fromParts.getFullYear());
// Output: 2023
// Output: 2026The two overload signatures declare that this function accepts either one argument or three arguments. The implementation signature uses optional parameters to handle both cases in a single body.
The key rule: the implementation signature is not callable. Even though it has optional parameters, you cannot call makeDate with two arguments:
const invalid = makeDate(2026, 7);Neither overload signature accepts exactly two arguments, so the compiler rejects the call even though the implementation signature technically has room for it:
No overload expects 2 arguments, but overloads do exist that expect either 1 or 3 arguments.Only the overload signatures determine what callers can do. The implementation signature is an internal detail.
When Overloads Are Useful
Overloads shine when different argument counts produce different return types. A union parameter cannot express this:
const settings: Record<string, string> = { theme: "dark" };
// Overloads: return type depends on the argument
function getValue(key: string): string | undefined;
function getValue(key: string, defaultValue: string): string;
function getValue(key: string, defaultValue?: string): string | undefined {
const value = settings[key];
if (value !== undefined) return value;
return defaultValue;
}Calling this function with one argument keeps the possibly-missing type, while calling it with a default argument narrows the return type to a guaranteed string:
const a = getValue("missing");
// Type: string | undefined
const b = getValue("missing", "Guest");
// Type: string, never undefined
console.log(a?.toUpperCase());
console.log(b.toUpperCase());
// Output: undefined
// Output: GUESTThe first overload returns string | undefined because the caller did not provide a default. The second overload returns a plain string only because the default guarantees a value. A single implementation with a union return type would lose this precision: every caller would see the possibly-undefined type regardless of whether they passed a default.
When to Use a Union Instead
Overloads have a limitation: TypeScript resolves a call to exactly one overload. A union argument that could match multiple overloads will fail even when the overloads logically cover it:
// Overloads for string and array
function len(value: string): number;
function len(value: unknown[]): number;
function len(value: string | unknown[]): number {
return value.length;
}
len("hello"); // OK
len([1, 2, 3]); // OK
len(Math.random() > 0.5 ? "hello" : [1, 2, 3]);The conditional expression could be either a string or an array, and that combined union type does not match either overload signature on its own, so the compiler rejects the call with both overloads listed as failures:
No overload matches this call.
Overload 1 of 2, '(value: string): number', gave the following error.
Argument of type '"hello" | number[]' is not assignable to parameter of type 'string'.
Type 'number[]' is not assignable to type 'string'.
Overload 2 of 2, '(value: unknown[]): number', gave the following error.
Argument of type '"hello" | number[]' is not assignable to parameter of type 'unknown[]'.
Type 'string' is not assignable to type 'unknown[]'.The expression Math.random() > 0.5 ? "hello" : [1, 2, 3] has the type string | number[]. This union does not match either individual overload. But a simple union parameter handles it correctly:
// Union parameter: works for all cases
function len(value: string | unknown[]): number {
return value.length;
}
len("hello"); // OK
len([1, 2, 3]); // OK
len(Math.random() > 0.5 ? "hello" : [1, 2, 3]); // OKThe rule from the TypeScript handbook: always prefer parameters with union types instead of overloads when possible. Use overloads only when the return type must change based on the argument count or when the relationship between argument types cannot be expressed as a union.
The Implementation Must Be Compatible
The implementation signature must accept all the argument combinations that the overload signatures declare. If the implementation is more restrictive than the overloads, TypeScript reports an error:
// Overload expects string
function format(value: string): string;
// Implementation only accepts number
function format(value: number): string {
return value.toString();
}The implementation only accepts numbers, but callers are promised they can pass a string, so TypeScript refuses to accept the mismatched pair:
This overload signature is not compatible with its implementation signature.The overload says callers can pass a string, but the implementation only handles number. The implementation must be at least as permissive as all overloads combined, the same compatibility rule covered in type function parameters in TypeScript.
A safe pattern: make the implementation parameters a union of all overload parameter types, and use optional parameters for arguments that not every overload provides:
function describe(value: string): string;
function describe(value: string, detailed: boolean): string;
// Implementation matches both overloads
function describe(value: string, detailed?: boolean): string {
if (detailed) {
return `Detailed: ${value}`;
}
return value;
}The implementation uses detailed?: boolean to cover both the one-argument and two-argument overloads.
Overloads Are Not for Callbacks
Do not use overloads to type a callback parameter. Callbacks already benefit from the parameter count compatibility rule: a callback with fewer parameters is always valid. If you find yourself writing overloads for a callback, simplify it to a union type or a single signature with optional parameters only when you genuinely sometimes skip arguments.
For more on this, see type callback functions in TypeScript.
Common Mistakes
Trying to call the implementation signature directly. Only overload signatures determine valid calls. If callers need a specific argument pattern, add an overload signature for it.
Using overloads when the return type is the same. If every overload returns the same type, replace them with a single signature using union parameters. The code is shorter and callers with union arguments work correctly.
Writing overloads that differ only by parameter type, not count. If the only difference is the type of a single parameter, use a union type on that parameter. Overloads for string | number are almost never the right approach.
Adding too many overloads. Two or three overloads are typical. If a function needs more than four, reconsider the design. The function may be doing too many things, or a different type-level approach like generics may be cleaner.
Rune AI
Key Insights
- Overload signatures describe each valid way to call the function.
- The implementation signature handles all cases but is never callable from outside.
- The implementation must be compatible with all overload signatures.
- Prefer union parameters over overloads when the return type is the same.
- Use overloads when argument count changes the return type.
- Never use overloads for callbacks: declare all parameters as required instead.
Frequently Asked Questions
What is the difference between overload signatures and the implementation signature?
When should I use overloads instead of union types?
Can the implementation signature be called directly?
Conclusion
Function overloads let a function accept different argument counts and types while maintaining precise return types. Write overload signatures above the implementation, one per valid call pattern. The implementation signature handles all cases internally but is never callable. Overloads are a specialized tool: when the return type is the same across all calls, use a union parameter instead. When argument count drives the return type, overloads are the right choice.
More in this topic
TypeScript Compiler Explained
The TypeScript compiler (tsc) turns .ts files into .js files while checking types. Learn what it does, how to use it, and how it fits your workflow.
Set Up ESLint for TypeScript
Install and configure ESLint with typescript-eslint to catch bugs and enforce consistent code style in your TypeScript project.
Pick Utility Type in TypeScript
Pick creates a new type by selecting only the properties you name from an existing type. Use it to build focused types for specific views, API responses, or function parameters.