TypeScript function overloading is a way of defining multiple function signatures for a single function implementation. It allows you to specify different combinations of parameter types and return types for a function.
Function overloading can help you describe the relationship between the parameter types and the return type more accurately than using union types or any type. For example, if you have a function that can add two numbers or two strings, you can use function overloading to tell TypeScript that the function returns a number when the parameters are numbers and returns a string when the parameters are strings.
To use function overloading, you need to write one or more function declarations with different parameter types and return types before the function implementation. The function implementation must be compatible with all the overloaded signatures. For example:
// Function declarations (overloads)
function foo(a: number, b: number): number;
function foo(a: string, b: string): string;
// Function implementation
function foo(a: any, b: any): any {
return a + b;
}
In this example, the foo
function has two overloaded signatures: one for numbers and one for strings. The function implementation uses any type to accept any kind of values and returns any type as well. TypeScript will check that the function implementation matches all the overloaded signatures.
When you call an overloaded function, TypeScript will use the first matching signature based on the argument types. For example:
let x = foo(1, 2); // x is of type number
let y = foo("a", "b"); // y is of type string
let z = foo(true, false); // error: no matching signature
In this example, TypeScript infers that x is a number and y is a string based on the overloaded signatures. However, it reports an error when calling foo
with boolean arguments because there is no matching signature for that case.
// Overload signatures
function foo(x: string, y: string): string[];
function foo(x: number, y: number): number[];
// Implementation signature (not visible from the outside)
function foo(x: string | number, y: string | number): (string | number)[] {
return [x, y];
}
// Usage
let d = foo("foo", "bar"); // d is a string[]
let e = foo(1, 2); // e is a number[]
let f = foo("foo", 2); // error, no overload matches this call
When writing an overloaded function, you should always have two or more signatures above the implementation of the function.
// Function declarations (overloads)
function foo(a: number, b: number): number;
function foo(a: number, b: number, c: number): number;
// Function implementation
function foo(a: number, b: number, c?: number): number {
if (c) return a + b + c;
return a + b;
}
In this example, the foo
function has two overloaded signatures: one for two numbers and one for three numbers. The third parameter is optional in the function implementation. TypeScript will check that the function implementation matches both overloaded signatures.
Let’s consider a function that returns the length of a string or an array:
function foo(s: string): number;
function foo(arr: any[]): number;
function foo(x: any) {
return x.length;
}
let a = foo(""); // OK
let b = foo([0]); // OK
let c = foo(Math.random() > 0.5 ? "hello" : [0]); // error: No overload matches this call
we can instead write a non-overloaded version of the function:
function foo(x: any[] | string) {
return x.length;
}
The advantage of using union types is that they are more concise and expressive than overloads. They also avoid some of the pitfalls of overloads, such as having to order them correctly and having to write compatible implementation signatures. Union types also work better with generic functions and type inference.