TypeScript Function Overloads
A single JavaScript function often needs to behave differently depending on what arguments you pass it — think of a function that accepts either a number of milliseconds or three separate date parts. In plain JavaScript you’d just check typeof inside the function and hope callers pass sensible combinations. TypeScript lets you describe those different “shapes” of a call explicitly using function overloads, so the compiler can check every call site and give you the exact, narrowed return type for the arguments you actually passed.
Overview: How Function Overloads Work
A function overload is a set of multiple call signatures declared for the same function name, followed by one real function body called the implementation signature. The overload signatures have no body — they are pure declarations. The implementation signature is the only one with a body, and it is never directly visible to code that calls the function from outside.
When you call an overloaded function, the TypeScript compiler does not look at the implementation signature to decide whether your call is valid. Instead, it walks through the overload signatures in the order you wrote them, top to bottom, and uses the first one whose parameter types match your arguments. If none of the overload signatures match, you get a compile error — even if the implementation signature technically could have handled the call.
This means overload order matters a lot, and it also means the implementation signature exists purely as internal plumbing: it must be general enough (usually with union types and optional parameters) to accept anything any of the overloads could throw at it, but callers never see it as a distinct option.
It’s also worth being clear about what overloads are not: they are not runtime polymorphism like you might find in Java or C#. TypeScript performs no runtime dispatch based on argument types. There is exactly one JavaScript function emitted to the compiled output; the overload signatures are pure type-level declarations and are completely erased when the code is compiled — the emitted JavaScript is indistinguishable from a normal function with an if/typeof check inside it. All of the safety overloads give you exists only at compile time, inside the editor and during tsc checks.
Syntax
The general shape of an overloaded function looks like this:
function fnName(paramsA: TypesA): ReturnA;
function fnName(paramsB: TypesB): ReturnB;
function fnName(params: TypesA | TypesB): ReturnA | ReturnB {
// implementation body
}
| Part | Meaning |
|---|---|
| Overload signatures | One or more declarations (no body) listing the exact parameter/return combinations callers are allowed to use. |
| Implementation signature | The final declaration that has a body. Its parameter types must be compatible with (usually a union/superset of) every overload signature. |
| Call-site resolution | TypeScript matches your call against the overload signatures in order, top-to-bottom, and uses the first match — never the implementation signature directly. |
Overloads can also be written on class methods and object/interface methods using the same pattern — repeat the method name with different signatures, then one implementation.
Examples
Example 1: A date constructor helper
This function accepts either a single millisecond timestamp, or three separate numbers for month, day, and year.
function makeDate(timestamp: number): Date;
function makeDate(month: number, day: number, year: number): Date;
function makeDate(monthOrTimestamp: number, day?: number, year?: number): Date {
if (day !== undefined && year !== undefined) {
return new Date(year, monthOrTimestamp, day);
}
return new Date(monthOrTimestamp);
}
const single = makeDate(1690000000000);
const specific = makeDate(6, 15, 2024);
console.log(single instanceof Date, specific instanceof Date);
console.log(specific.getFullYear(), specific.getMonth(), specific.getDate());
Output:
true true
2024 6 15
Notice that you can call makeDate with one argument or three, but not two — TypeScript rejects any combination that doesn’t match one of the two declared overloads, even though the implementation signature’s optional parameters would technically allow it.
Example 2: Overloading with generics
Overload signatures can mix concrete types and generics. Here, reversing a string returns a string, while reversing an array returns an array of the same element type.
function reverse(value: string): string;
function reverse<T>(value: T[]): T[];
function reverse<T>(value: string | T[]): string | T[] {
if (typeof value === "string") {
return value.split("").reverse().join("");
}
return value.slice().reverse();
}
const reversedText = reverse("TypeScript");
const reversedNums = reverse([1, 2, 3, 4]);
console.log(reversedText);
console.log(reversedNums);
Output:
tpircSepyT
[ 4, 3, 2, 1 ]
Because of the overloads, reversedText is inferred as string and reversedNums is inferred as number[] — no manual casting needed, even though the implementation itself works with a union type internally.
Example 3: Overloaded class methods
Overloads work the same way on methods. Here a Formatter class formats numbers with two decimal places and dates as ISO strings.
class Formatter {
format(value: number): string;
format(value: Date): string;
format(value: number | Date): string {
if (value instanceof Date) {
return value.toISOString();
}
return value.toFixed(2);
}
}
const formatter = new Formatter();
console.log(formatter.format(3.14159));
console.log(formatter.format(new Date(Date.UTC(2024, 0, 1))));
Output:
3.14
2024-01-01T00:00:00.000Z
From outside the class, only the two overload signatures are visible — you cannot call formatter.format() with, say, a boolean, even though the implementation’s parameter type is a union that a boolean could theoretically be checked against.
Under the Hood: Overload Resolution and Erasure
- When you write a call like
makeDate(6, 15, 2024), the compiler scans the overload signature list from top to bottom. - It checks whether your argument list is assignable to the first overload’s parameters. If not, it moves to the next overload, and so on.
- The first overload that matches determines the parameter types you get autocomplete/checking against, and the return type of the expression.
- If no overload matches, TypeScript reports “No overload matches this call” and, depending on how close you were, lists which overload(s) failed and why.
- The implementation signature is never checked against your call directly — it’s only checked against the body’s own logic, and against each overload signature to make sure it can legally handle everything the overloads promise.
- At compile time,
tscstrips every overload declaration and the implementation signature’s type annotations. The emitted JavaScript contains a single function definition with no trace of the overload list — this is type erasure in action. Two different projects, one written with overloads and one written with a hand-rolledtypeofcheck and no types at all, can emit byte-for-byte identical JavaScript.
Common Mistakes
Mistake 1: Calling with an argument combination no overload allows
Even though the implementation signature uses any, callers are restricted to the declared overloads only:
function add(a: number, b: number): number;
function add(a: string, b: string): string;
function add(a: any, b: any): any {
return a + b;
}
const result = add(1, "2");
This fails with “No overload matches this call” because neither overload allows a number and a string together — the implementation signature’s permissive any types are irrelevant to call-site checking. The fix is to either add a third overload for the mixed case (if that’s really intended) or convert both arguments to the same type before calling: add(1, Number("2")).
Mistake 2: Implementation signature that doesn’t cover every overload
function toArray(value: string): string[];
function toArray(value: number): number[];
function toArray(value: boolean): string[] | number[] {
return [];
}
This produces “This overload signature is not compatible with its implementation signature” on both overload declarations. The implementation only accepts boolean, but it must be able to accept everything the overloads promise — string and number — since those are the types callers are actually allowed to pass. The fix is to widen the implementation’s parameter type to string | number (or a broader compatible type) so it genuinely covers both overloads.
Best Practices
- Prefer a single function with a union parameter type (
value: string | number) over overloads whenever the logic and return type don’t actually depend on narrowing — overloads add real complexity and should be reserved for cases where the return type genuinely changes based on input shape. - Order overload signatures from most specific to least specific; since resolution stops at the first match, a broad signature placed first can accidentally shadow a more specific one.
- Keep the implementation signature private in spirit — never rely on callers being able to use its exact parameter types, since only the overload signatures are checked at call sites.
- Avoid overloading purely to make parameters optional; use optional parameters (
b?: number) or default values instead, which don’t require multiple signatures. - When two overloads only differ by an optional trailing parameter, TypeScript can usually infer a single combined signature — don’t add overloads unless the parameter types or the return type actually differ.
- Document each overload with a short comment explaining which call shape it supports, especially once you have three or more — overload lists get hard to scan quickly.
- Use generics inside overloads (as in the
reverseexample) instead of duplicating near-identical overloads for every concrete type.
Practice Exercises
- Exercise 1: Write an overloaded function
createIdthat returns astringwhen called with no arguments (a random-looking prefixed id) and returns anumberwhen called with a single numericseedargument. Implement it however you like internally. - Exercise 2: Write an overloaded function
parseValuewith two overloads: one that takes astringand returns anumber(parsed withNumber()), and one that takes astring[]and returns anumber[]. Call both forms and log the results. - Exercise 3: Take the broken
toArrayexample from the Common Mistakes section and fix it so it type-checks under--strict, correctly wrapping astringin a one-elementstring[]and anumberin a one-elementnumber[].
Summary
- Function overloads let one function name have multiple declared call signatures, each with its own parameter and return types.
- Only the overload signatures are visible to callers; the implementation signature (the one with a body) is used internally and must be compatible with every overload.
- TypeScript resolves calls by checking overload signatures top-to-bottom and using the first match — order matters, so put more specific overloads first.
- All overload information is erased at compile time; the emitted JavaScript is a single ordinary function with no trace of the overloads.
- Prefer union types over overloads unless the return type genuinely needs to change based on the shape of the arguments.
