skip to content
Nikolas Barwicki - Javascript Blog Nikolas's Blog

Mastering function overloading in Typescript

/ 7 min read

Introduction

Have you ever found yourself in a situation where you need a flexible function that can accept either a string or an array of strings as an argument and return either a string or an array of strings depending on the type of the argument?

Function overloading in TypeScript is a powerful feature that can help you solve this problem. In this article, we will explore how to use function overloading in TypeScript to write more expressive and flexible code, handle different combinations of parameter types and/or counts, and make your code more readable and maintainable.

How to use it?

To use function overloading, you can define multiple function signatures with the same name but different parameters. For example:

function add(a: number, b: number): number
function add(a: string, b: string): string
function add(a: any, b: any): any {
  return a + b
}

In this example, the add function is overloaded with two signatures: one that takes two numbers and returns a number, and another that takes two strings and returns a string. The implementation of the function (the last function definition) can handle any type of input and simply adds the two parameters together.

When the add function is called with a set of parameters, TypeScript will try to find the best match based on the parameter types, and call the corresponding implementation.

For example:

console.log(add(1, 2)) // 3
console.log(add('Hello, ', 'world!')) // "Hello, world!"

In the first call to add, TypeScript matches the first function signature (two numbers) and calls the implementation with the parameters 1 and 2, resulting in the output 3. In the second call, TypeScript matches the second function signature (two strings) and calls the implementation with the parameters “Hello, ” and “world!”, resulting in the output “Hello, world!“.

More complex examples

In the first example, the concatenate function is overloaded with two signatures: one that takes two strings and returns a string, and another that takes three strings and returns a string.

// Overload with different number of parameters
function concatenate(a: string, b: string): string
function concatenate(a: string, b: string, c: string): string
function concatenate(a: any, b: any, c?: any): any {
  if (c) {
    return a + b + c
  } else {
    return a + b
  }
}
console.log(concatenate('hello', 'world')) // "helloworld"
console.log(concatenate('hello', 'world', '!')) // "helloworld!"

In the second example, the divide function is overloaded with two signatures, one that takes two number and returns a number and another that takes two strings and returns a string.

// Overload with different parameter types
function divide(a: number, b: number): number
function divide(a: string, b: string): string
function divide(a: any, b: any): any {
  if (typeof a === 'number' && typeof b === 'number') {
    return a / b
  } else {
    return a + ' / ' + b
  }
}
console.log(divide(10, 2)) // 5
console.log(divide('10', '2')) // "10 / 2"

And lastly the most complex example in this article.

It handles three different sets of parameters. You can easily say that this function is a wrong example because this function has too many resposibilites - and you’re probably right. This example is meant to show how many posibilites using function overloading can give you when using Typescipt:

// Overload with different parameter types and rest parameters
interface Person {
  name: string
  age: number
}
function createPerson(name: string, age: number): Person
function createPerson(options: { name: string; age: number }): Person
function createPerson(
  options: { name: string; age: number },
  ...hobbies: string[]
): Person
function createPerson(a: any, b?: any, ...c: any[]): any {
  if (typeof b === 'number') {
    return { name: a, age: b }
  } else if (typeof b === 'undefined' && typeof a === 'object') {
    return { name: a.name, age: a.age }
  } else {
    return { name: a.name, age: a.age, hobbies: c }
  }
}
console.log(createPerson('John', 30)) // { name: "John", age: 30 }
console.log(createPerson({ name: 'Jane', age: 25 })) // { name: "Jane", age: 25 }
console.log(
  createPerson({ name: 'Bob', age: 35 }, 'reading', 'coding', 'traveling')
) // { name: "Bob", age: 35, hobbies: ["reading", "coding", "traveling"] }

Defining and typing a function multiple times

You can also use a type or an interface to describe the different overloads of a function and avoid repeating the function signature and return type multiple times:

interface Add {
  (a: number, b: number): number
  (a: string, b: string): string
}
let add: Add = (a: any, b: any): any => {
  return a + b
}

Alternatively, you can use a type to describe the different overloads of a function:

type Add = {
  (a: number, b: number): number
  (a: string, b: string): string
}
let add: Add = (a: any, b: any): any => {
  return a + b
}
console.log(add(1, 2)) // 3
console.log(add('Hello, ', 'world!')) // "Hello, world!"

By using a type or an interface to describe the different overloads of a function, you can avoid repeating the function signature and return type multiple times, and it makes the code more readable and maintainable.

Additionally, the TypeScript compiler will check that the implementation of the function is consistent with the overloads defined in the type or interface, ensuring that the function is always called with the correct parameter types and counts.

Passing wrong type or fewer arguments than expected

When you provide an argument of the wrong type or fewer arguments than expected to a function that is overloaded in TypeScript, the TypeScript compiler will try to find the best match among the available overloads based on the parameter types and counts. If it can’t find a match, it will throw a compile-time error.

Providing an argument of the wrong type:

function concatenate(a: string, b: string): string
function concatenate(a: number, b: number): number
function concatenate(a: any, b: any): any {
  return a + b
}
console.log(concatenate('hello', 5)) // compile-time error

Here, in this example the function concatenate is overloaded with two signatures: one that takes two strings and returns a string, and another that takes two numbers and returns a number. The call to the function with the arguments “hello” and 5 will result in a compile-time error, because the TypeScript compiler can’t find a match between the provided arguments and the available overloads.

Providing less arguments than expected:

function divide(a: number, b: number): number
function divide(a: number, b: number, c: number): number
function divide(a: any, b: any, c?: any): any {
  return a / b
}
console.log(divide(10)) // compile-time error

Here, in this example the function divide is overloaded with two signatures: one that takes two numbers and returns a number, and another that takes three numbers and returns a number. The call to the function with only one argument will result in a compile-time error, because the TypeScript compiler can’t find a match between the provided arguments and the available overloads.

Providing more arguments than expected:

function multiply(a: number, b: number, c?: number): number
function multiply(a: number, b: number): number
function multiply(a: any, b: any, c?: any): any {
  return a * b * c
}
console.log(multiply(2, 3, 4, 5)) // compile-time error

Here, in this example the function multiply is overloaded with two signatures: one that takes two numbers and returns a number and another that takes three numbers and returns a number. The call to the function with four arguments will result in a compile-time error, because the TypeScript compiler can’t find a match between the provided arguments and the available overloads.

In all these examples, the TypeScript compiler can’t find a match between the provided arguments and the available overloads, and it will throw a compile-time error. It’s important to keep in.

Conclusion

In summary, function overloading in TypeScript is a powerful feature that allows you to write more expressive and flexible code, group related functions under the same name and handle different combinations of parameter types and/or counts.