TypeScript Function Overloading

May 17, 2023#typescript

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.

  1. The signature of the implementation is not visible from the outside. It means that when you write a function with multiple overload signatures, the callers of the function can only see the overload signatures, not the implementation signature.
// 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.

  1. Function overloading can also be used with optional parameters or rest parameters, but you need to make sure that the number of required parameters is consistent among all the overloaded signatures. For example:
// 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.

  1. Prefer parameters with union types instead of overloads when possible. It means that instead of writing multiple overload signatures for a function that can accept different types of arguments, it is better to use a single signature with parameters that have union types.

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.