Post cover

A Simple Explanation of Function Overloading in TypeScript

Posted November 18, 2021

Most of the functions accept a fixed set of arguments.

But some functions can accept a variable number of arguments, arguments of different types, and could even return different types depending on how you invoke the function. To annotate such function TypeScript offers the function overloading feature.

Let's see how function overloading works.

1. The function signature

Let's consider a function that returns a welcome message to a particular person:

ts
function greet(person: string): string {
return `Hello, ${person}!`;
}

The function above accepts 1 argument of type string: the name of the person.

Invoking the function is pretty simple:

ts
greet('World'); // 'Hello, World!'

What if you'd like to make the greet() function more flexible? For example, make it additionally accept a list of persons to greet.

Such a function would accept a string or an array of strings as an argument, as well as return a string or an array of strings.

How to annotate such a function? There are 2 approaches.

The first approach is straightforward and involves modifying the function signature directly by updating the parameter and return types.

Here's how greet() looks after updating the parameter and return types:

ts
function greet(person: string | string[]): string | string[] {
if (typeof person === 'string') {
return `Hello, ${person}!`;
} else if (Array.isArray(person)) {
return person.map(name => `Hello, ${name}!`);
}
throw new Error('Unable to greet');
}

Now you can invoke greet() in 2 ways:

ts
greet('World'); // 'Hello, World!'
greet(['Jane', 'Joe']); // ['Hello, Jane!', 'Hello, Joe!']

Updating the function signature directly to support the multiple ways of invocation is the usual and a good approach.

However, there are situations when you might want to take an alternative approach and define separately all the ways your function can be invoked. This approach is called function overloading.

2. The function overloading

The second approach is to use the function overloading feature. I recommend it when the function signature is relatively complex and has multiple types involved.

Putting the function overloading in practice requires defining the so-called overload signatures and an implementation signature.

The overload signature defines the parameter and return types of the function, and doesn't have a body. A function can have multiple overload signatures: corresponding to the different ways you can invoke the function.

The implementation signature, on the other side, also has the parameter types and return type, but also a body that implements the function. There can be only one implementation signature.

Let's transform the function greet() to use the function overloading:

ts
// Overload signatures
function greet(person: string): string;
function greet(persons: string[]): string[];
 
// Implementation signature
function greet(person: unknown): unknown {
if (typeof person === 'string') {
return `Hello, ${person}!`;
} else if (Array.isArray(person)) {
return person.map(name => `Hello, ${name}!`);
}
throw new Error('Unable to greet');
}

The greet() function has 2 overload signatures and one implementation signature.

Each overload signature describes one way the function can be invoked. In the case of greet() function, you can call it 2 ways: with a string argument, or with an array of strings argument.

The implementation signature function greet(person: unknown): unknown { ... } contains the proper logic how the function works.

Now, as before, you can invoke greet() with the arguments of type string or array of strings:

ts
greet('World'); // 'Hello, World!'
greet(['Jane', 'Joe']); // ['Hello, Jane!', 'Hello, Joe!']

2.1 Overload signatures are callable

While the implementation signature implements the function behavior, however, it is not directly callable. Only the overload signatures are callable.

ts
greet('World'); // Overload signature callable
greet(['Jane', 'Joe']); // Overload signature callable
 
const someValue: unknown = 'Unknown';
greet(someValue); // Implementation signature NOT callable
No overload matches this call. Overload 1 of 2, '(person: string): string', gave the following error. Argument of type 'unknown' is not assignable to parameter of type 'string'. Overload 2 of 2, '(persons: string[]): string[]', gave the following error. Argument of type 'unknown' is not assignable to parameter of type 'string[]'.2769No overload matches this call. Overload 1 of 2, '(person: string): string', gave the following error. Argument of type 'unknown' is not assignable to parameter of type 'string'. Overload 2 of 2, '(persons: string[]): string[]', gave the following error. Argument of type 'unknown' is not assignable to parameter of type 'string[]'.

In the example above you cannot call greet() function with an argument of type unknown (greet(someValue)), even if the implementation signature accepts unknown argument.

2.2 Implementation signature must be general

Be aware that the implementation signature type should be generic enough to include the overload signatures.

Otherwise, TypeScript won't accept the overload signature as being incompatible.

For example, if you modify the implementation signature's return type from unknown to string:

ts
// Overload signatures
function greet(person: string): string;
function greet(persons: string[]): string[];
This overload signature is not compatible with its implementation signature.2394This overload signature is not compatible with its implementation signature.
 
// Implementation signature
function greet(person: unknown): string {
// ...
throw new Error('Unable to greet');
}

Then the overload signature function greet(persons: string[]): string[] is marked as being incompatible with function greet(person: unknown): string.

string return type of the implementation signature isn't general enough to be compatible with string[] return type of the overload signature.

3. Method overloading

While in the previous examples the function overloading was applied to a regular function. But you can overload a methods too!

During method overloading, both the overload signatures and implementation signature are now a part of the class.

For example, let's implement a Greeter class, with an overload method greet():

ts
class Greeter {
message: string;
 
constructor(message: string) {
this.message = message;
}
 
// Overload signatures
greet(person: string): string;
greet(persons: string[]): string[];
 
// Implementation signature
greet(person: unknown): unknown {
if (typeof person === 'string') {
return `${this.message}, ${person}!`;
} else if (Array.isArray(person)) {
return person.map(name => `${this.message}, ${name}!`);
}
throw new Error('Unable to greet');
}
}

The Greeter class contains greet() overload method: 2 overload signatures describing how the method can be called, and the implementation signature containing the proper implementation.

Thanks to method overloading you can call hi.greet() in 2 ways: using a string or using an array of strings as argument.

ts
const hi = new Greeter('Hi');
 
hi.greet('Angela'); // 'Hi, Angela!'
hi.greet(['Pam', 'Jim']); // ['Hi, Pam!', 'Hi, Jim!']

4. When to use function overloading

Function overloading, when used the right way, can greatly increase the usability of functions that may be invoked in multiple ways. That especially useful during autocomplete: you get listed all the possible overloadings as separate records in autocomplete.

However, there are situations when I'd recommend not to use the function overloading, but rather stick to the function signature.

For example, don't use the function overloading for optional parameters:

ts
// Not recommended
function myFunc(): string;
function myFunc(param1: string): string;
function myFunc(param1: string, param2: string): string;
function myFunc(...args: string[]): string {
// implementation...
}

Using the optional parameters in the function signature should be enough:

ts
// OK
function myFunc(param1?: string, param2: string): string {
// implementation...
}

For more details check Function overloading Do's and Don'ts.

5. Conclusion

Function overloading in TypeScript lets you define functions that can be called in multiple ways.

Using function overloading requires defining the overload signatures: a set of functions with parameter and return types, but without a body. These signatures indicate how the function should be invoked.

Additionally, you have to write the proper implementation of the function (implementation signature): the parameter and return types, as well the function body. Note that the implementation signature is not callable.

Aside from regular functions, methods in classes can be overload too.

Like the post? Please share!

Quality posts into your inbox

I regularly publish posts containing:

  • Important JavaScript concepts explained in simple words
  • Overview of new JavaScript features
  • How to use TypeScript and typing
  • Software design and good coding practices

Subscribe to my newsletter to get them right into your inbox.

Join 4716 other subscribers.
Dmitri Pavlutin

About Dmitri Pavlutin

Tech writer and coach. My daily routine consists of (but not limited to) drinking coffee, coding, writing, coaching, overcoming boredom 😉.
Email addressTwitter profileFacebook pageLinkedIn profile