Function overloading in TypeScript means giving one function several call signatures, each with its own parameter types and return type, and a single body that handles all of them. The compiler checks every call against the list of signatures and picks the first one that fits, so the caller gets a precise return type for the arguments it passed.
We use overloads when the return type depends on the argument types, or when a function accepts a few fixed argument shapes that a single signature cannot describe. For example, a getUser() function in a REST client returns one user for an ID and a list of users for an array of IDs.
The following example shows a minimal overloaded function and an overloaded class method, which together cover the whole syntax.
// 1. Overload signatures (what callers see)
function greet(name: string): string;
function greet(names: string[]): string[];
// 2. Implementation signature (one body for all overloads)
function greet(input: string | string[]): string | string[] {
if (typeof input === "string") {
return "Hello, " + input;
}
return input.map((name) => "Hello, " + name);
}
// 3. Each call gets the return type of the matching overload
const one = greet("Lokesh"); // one = "Hello, Lokesh" (string)
const many = greet(["Raj", "John"]); // many = ["Hello, Raj", "Hello, John"] (string[])
// 4. Overloaded method in a class
class Person {
constructor(public name: string, public age: number) {}
olderThan(age: number): boolean;
olderThan(other: Person): boolean;
olderThan(x: number | Person): boolean {
return this.age > (typeof x === "number" ? x : x.age);
}
}
const lokesh = new Person("Lokesh", 37);
const john = new Person("John", 40);
const r1 = lokesh.olderThan(35); // r1 = true
const r2 = lokesh.olderThan(john); // r2 = false
Notice that one has the type string and many has the type string[], although both calls run the same body.
TypeScript overloading is different from overloading in Java or C#, where each overload is a separate method with its own body. JavaScript has only one function per name, so TypeScript overloads exist only at compile time. We write several signatures, but there is still one implementation, and that implementation checks the arguments itself.
Next, we look at overload and implementation signatures, overloads with a different number of parameters, the order rules, constructor and method overloads in classes, overloads in function types and interfaces, and the cases where a union type, an optional parameter or a generic is the better choice.
1. Overload Signatures and the Implementation Signature
An overloaded function has two kinds of signatures.
- Overload signatures are declarations without a body that describe the ways callers may call the function.
- The implementation signature is the last one, which has the body.
The implementation signature’s parameter types must be wide enough to accept the arguments of every overload, which is why it often uses union types such as string | string[].
// 1. Overload signatures
function greet(name: string): string;
function greet(names: string[]): string[];
// 2. Implementation signature and body
function greet(input: string | string[]): string | string[] {
if (typeof input === "string") {
return "Hello, " + input;
}
return input.map((name) => "Hello, " + name);
}
// 3. Calls
const one = greet("Lokesh"); // one = "Hello, Lokesh" (string)
const many = greet(["Raj", "John"]); // many = ["Hello, Raj", "Hello, John"] (string[])
const loud = one.toUpperCase(); // loud = "HELLO, LOKESH"
const count = many.length; // count = 2
We can see the benefit in step 3. Because one has the type string, we can call toUpperCase() without a check. Without overloads, the function would have only the signature (input: string | string[]): string | string[], and every caller would need a typeof or Array.isArray() check before using the result. The overloads also show that different overloads may return different types; they do not need the same return type.
Inside the body, TypeScript does not know which overload the caller used. The typeof check narrows input to string in the first branch and to string[] after it. Writing that check correctly is our job. The compiler does not verify that the body returns a string for the first overload.
1.1. Callers Cannot See the Implementation Signature
The implementation signature is not part of the function’s public type. Callers can use only the overload signatures, even though the body accepts the union. The rule matters when the argument itself is a union.
greet(42);
const input = Math.random() > 0.5 ? "Lokesh" : ["Raj"];
greet(input);
Both calls fail. The first passes a number, which no overload accepts, and the second passes a value whose type is “Lokesh” | string[], and each overload accepts only one of the two types, while no single overload accepts both.
error TS2769: No overload matches this call.
The last overload gave the following error.
Argument of type 'number' is not assignable to parameter of type 'string[]'.
error TS2769: No overload matches this call.
The last overload gave the following error.
Argument of type '"Lokesh" | string[]' is not assignable to parameter of type 'string[]'.
Type 'string' is not assignable to type 'string[]'.
When callers often hold such a union, we add a third overload greet(input: string | string[]): string | string[] at the end of the list, or we drop the overloads and use the union signature alone, as in section 6.
2. Overloads With a Different Number of Parameters
Overloads can also differ in the number of parameters. For example, a date function from the TypeScript Handbook accepts either a timestamp or a year, month and day. Two arguments make no sense for that function, and overloads let us forbid the two-argument call.
function makeDate(timestamp: number): Date;
function makeDate(year: number, month: number, day: number): Date;
function makeDate(yearOrTime: number, month?: number, day?: number): Date {
if (month !== undefined && day !== undefined) {
return new Date(Date.UTC(yearOrTime, month, day));
}
return new Date(yearOrTime);
}
const d1 = makeDate(0); // d1 = 1970-01-01T00:00:00.000Z
const d2 = makeDate(2026, 0, 15); // d2 = 2026-01-15T00:00:00.000Z
The implementation marks month and day as optional with ?, because the one-argument overload does not pass them. Month numbers in JavaScript dates start at 0, so 0 is January. A call with two arguments fails, and the error message lists the argument counts that do exist.
error TS2575: No overload expects 2 arguments, but overloads do exist that expect either 1 or 3 arguments.
A single signature with two optional parameters would accept makeDate(2026, 0) and create the wrong date without any error. Forbidden argument combinations like this one are where overloads add safety. For arguments that callers may leave off at the end, optional and default parameters are enough.
3. Rules the Compiler Enforces
Most overload mistakes produce a compile error with a clear message. Each rule has its own error code, so the code in the message points to the broken rule.
| Rule | Error when broken |
|---|---|
| Every overload must be compatible with the implementation signature | TS2394: This overload signature is not compatible with its implementation signature. |
| The implementation must come right after the overloads, with no other code between them | TS2391: Function implementation is missing or not immediately following the declaration. |
| All overloads must be exported, or none of them | TS2383: Overload signatures must all be exported or non-exported. |
| A call must match at least one overload | TS2769: No overload matches this call. |
The first rule is the one we hit most. If we add an overload bad(age: number): number but the implementation still accepts only a string, the new overload has no body that can handle it, and the compiler reports TS2394 on that overload.
3.1. Order the Overloads From Specific to General
TypeScript checks overloads from top to bottom and stops at the first match. If a general overload comes first, the more specific ones below it are never chosen. The compiler does not report the wrong order, so the bug shows up only as a wider type than expected.
// 1. Wrong order: the general overload comes first
function parseWrong(value: unknown): unknown;
function parseWrong(value: string): number;
function parseWrong(value: unknown): unknown {
return typeof value === "string" ? Number(value) : value;
}
const a = parseWrong("37"); // a = 37, but type unknown
// 2. Right order: the specific overload comes first
function parse(value: string): number;
function parse(value: unknown): unknown;
function parse(value: unknown): unknown {
return typeof value === "string" ? Number(value) : value;
}
const b = parse("37"); // b = 37 (number)
In the wrong version, a string matches value: unknown first, so a has the type unknown and a + 1 would not compile. We put more specific overloads before more general ones, which is the ordering rule for overloads in the TypeScript declaration file guidelines.
4. Constructor and Method Overloads in Classes
Class methods and constructors use the same syntax, with the overload signatures right above the implementation inside the class body. Constructor overloads are a common way to let callers create an object from different inputs, for example from separate values or from a plain object. A Person can then come from a form with two fields or from a JSON object returned by an API.
class Person {
name: string;
age: number;
// 1. Constructor overloads
constructor(name: string, age: number);
constructor(data: { name: string; age: number });
constructor(nameOrData: string | { name: string; age: number }, age?: number) {
if (typeof nameOrData === "string") {
this.name = nameOrData;
this.age = age ?? 0;
} else {
this.name = nameOrData.name;
this.age = nameOrData.age;
}
}
// 2. Method overloads
olderThan(age: number): boolean;
olderThan(other: Person): boolean;
olderThan(ageOrPerson: number | Person): boolean {
const limit = typeof ageOrPerson === "number" ? ageOrPerson : ageOrPerson.age;
return this.age > limit;
}
}
const lokesh = new Person("Lokesh", 37);
const john = new Person({ name: "John", age: 40 });
const r1 = john.olderThan(lokesh); // r1 = true
const r2 = lokesh.olderThan(40); // r2 = false
The constructor overloads make new Person(“Lokesh”) a compile error, because no overload takes a single string. The implementation still has to handle the missing age for the type checker (age ?? 0), since its own signature marks it optional.
A subclass that overrides an overloaded method must still accept every overload.
An override with only olderThan(age: number) fails with TS2416: Property ‘olderThan’ in type ‘Employee’ is not assignable to the same property in base type ‘Person’. The subclass method must accept every call that the base method accepts, which is the general rule for method overriding and for derived classes.
5. Overloads in Function Types and Interfaces
Arrow functions and function expressions cannot declare overloads themselves. Instead, we describe the overloads in a type with several call signatures (a call signature is the part in parentheses that says how to call a value) and assign a function to it. Interfaces can declare overloaded methods the same way, where the method name appears once per overload. For example, a reporting app can declare a Formatter interface with overloaded format() methods, and each report type provides one object that implements them.
// 1. A type with two call signatures
type Greet = {
(name: string): string;
(names: string[]): string[];
};
// 2. An overloaded function declaration matches it
function greetImpl(name: string): string;
function greetImpl(names: string[]): string[];
function greetImpl(input: string | string[]): string | string[] {
return typeof input === "string" ? "Hi, " + input : input.map((n) => "Hi, " + n);
}
const greet: Greet = greetImpl;
const one = greet("Raj"); // one = "Hi, Raj" (string)
const many = greet(["Raj", "John"]); // many = ["Hi, Raj", "Hi, John"] (string[])
// 3. Overloaded methods in an interface
interface Formatter {
format(value: number): string;
format(value: Date): string;
}
const formatter: Formatter = {
format(value: number | Date): string {
return typeof value === "number" ? value.toFixed(1) : value.toISOString().slice(0, 10);
},
};
const f1 = formatter.format(37); // f1 = "37.0"
const f2 = formatter.format(new Date(0)); // f2 = "1970-01-01"
Assigning an arrow function to Greet does not compile. An arrow function with the signature (input: string | string[]) => string | string[] could return an array for a string argument, and TypeScript rejects it with TS2322: Type ‘string \| string[]’ is not assignable to type ‘string’. The overloaded function declaration in step 2 passes the check, because its overload list matches the type. The interface in step 3 works with one method that accepts both types, because that method returns string for both overloads.
6. When a Union, an Optional Parameter or a Generic Is Better
Overloads add lines and a body that the compiler checks only loosely. We prefer parameters with union types instead of overloads when possible, which is also the advice in the TypeScript Handbook. We use overloads only when the return type changes with the argument types in a way that a simpler signature cannot express.
// 1. Union parameter: one signature, same return type
function len(value: string | unknown[]): number {
return value.length;
}
const input = Math.random() > 0.5 ? "apple" : ["apple", "banana"];
const size = len(input); // size = 5 or 2
// 2. Optional parameter: trailing arguments that may be missing
function label(name: string, age?: number): string {
return age === undefined ? name : name + " (" + age + ")";
}
const l1 = label("Raj"); // l1 = "Raj"
const l2 = label("Lokesh", 37); // l2 = "Lokesh (37)"
// 3. Generic: the return type is the argument type
function firstOf<T>(items: T[]): T | undefined {
return items[0];
}
const fruit = firstOf(["apple", "banana"]); // fruit = "apple" (string | undefined)
The len() version accepts a union argument, which an overloaded len(s: string) / len(arr: unknown[]) pair would reject with TS2769, as shown in section 1.1. The choice comes down to how the return type relates to the arguments.
| Situation | Use |
|---|---|
| Same return type for every argument type | A union parameter |
| Some trailing arguments may be left out | Optional or default parameters |
| The return type is the argument type, or built from it | A generic function |
| A few fixed argument shapes with different return types, or forbidden argument combinations | Overloads |
7. Running the Overloading Examples
The GitHub project holds the overload examples from section 1 to section 6, one file each. To build it, we need Node.js 22 or newer; npm install fetches TypeScript 7.0.2, and the project compiles in strict mode.
npm install
npm start
Every line of output belongs to one numbered comment in the snippets, grouped by section. The failing calls from section 1.1, section 2, section 3 and section 5 are not in the project, because the build must succeed.
8. Conclusion
TypeScript overloads are a compile-time feature, where several signatures describe how a function may be called, and one implementation with union parameters does the work. Overloads are worth their extra lines when the return type depends on the arguments or when some argument combinations must be rejected.
We order the overloads from specific to general and keep the implementation right below them. We write the argument checks in the body with care, because the compiler does not match them to the overloads. When every call returns the same type, a union parameter or an optional parameter is shorter and safer, and a generic covers return types built from the argument.
9. References
The TypeScript Handbook describes overloads for functions and classes, and the declaration file guidelines add the ordering advice.
- TypeScript Handbook: Function Overloads
- TypeScript Handbook: Writing Good Overloads
- TypeScript: Declaration Files Do’s and Don’ts (overload ordering)
- TypeScript Handbook: Classes
Happy Learning !!
what if i need to find Employee by name and location (3rd chapter of article)?
You can handle such cases inside the method. For example, if age is less than 0, then do not use it. And in method documentation, you can mention to pass 0 or -1 if user want to find employees just by name and location.
findEmployees("alexey", -1, "USA")I was thinking about it, but is it a best practice to use this solution?
Perhaps it is the only solution if you are not creating a new method.