What are Function overloads in Typescript


Borislav Hadzhiev

Last updated: Feb 26, 2021


Photo from Unsplash

The theory #

Function overloads allow us to specify multiple signatures for the same function - for example the same function gets called with strings and returns a string, but if it got called with numbers it returns a number.

Considering the fact that javascript is a dynamic language, we sometimes have situations where a function can take in different types of values and return different types of values. Let's say for example you are validating a user input - someone using postman can send in any value they want - then it's up to you to figure out how to validate it.

In such cases we would like to tell typescript - hey the exact same function can be called with types A, B and C, but I still want to take advantage of type checking because based on the type the function got called with, I can conclude the return type of the function.

The solution to this is to supply multiple function types for the same function as a list of overloads, in code:

function reverse(a: string): string; function reverse(a: string[]): string[]; function reverse(a: string | string[]): string | string[] { // your implementation }

In the above snippet we have a reverse function, which can reverse a string or an array of strings. And based on the passed in input it is going to return the same type but reversed.

By typing every possible scenario we help typescript infer the return value of the function, when we invoke it.

If we don't specify the possible scenarios, typescript has no way of knowing the return value, even though it seems obvious to the caller:

function printSum(a: string | number, b: string | number): string | number { if (typeof a === 'string' || typeof b === 'string') { return String(a) + String(b); } // At this point we know that both a and b are numbers return a + b; } // number is of type string | number const number = printSum(10, 5);

We know that by passing in two numbers to printSum we will get a number, but typescript has no way of knowing, so instead we get a union return type of string | number.

Complete Example #

type Comparable = Record<string, string> | string[]; function getLongerValue(a: Comparable, b: Comparable): Comparable { if (Array.isArray(a) && Array.isArray(b)) { return a.length > b.length ? a : b; } // we assume they passed objects because these are the only use cases // we intend to handle if (!('length' in a) && !('length' in b)) { return Object.keys(a).length > Object.keys(b).length ? a : b; } throw new Error("You didn't pass in arrays or objects"); } // ["hello", "world"] // notice longerArray is of type Comparable // even tho we know that by passing in 2 arrays we get an array back const longerArray = getLongerValue(['hello'], ['hello', 'world']); console.log(longerArray); // {name: 'Tom', profession: 'programmer'} // notice longerObject is of type Comparable // even tho we know that by passing in 2 objects we get an object back const longerObject = getLongerValue( {name: 'Tom'}, {name: 'Tom', profession: 'programmer'}, ); console.log(longerObject); // Error: You didn't pass in arrays or objects // ideally we would like to inform the user at dev time rather than runtime // that they shouldn't call this function with mixed inputs const longerMixed = getLongerValue(['hello'], { name: 'Tom', profession: 'programmer', }); console.log(longerMixed);

In the example above we notice some downsides - we haven't given typescript the help it needs to help us with type checking.

By passing in 2 arrays, comparing their length and returning the longer array, we can be sure that if we invoke the function with 2 arrays, we get an array back, however with our current implementation we get an object or array type back.

Let's add our function overloads to help typescript help us:

function getLongerValue( a: Record<string, string>, b: Record<string, string>, ): Record<string, string>; function getLongerValue(a: string[], b: string[]): string[]; function getLongerValue(a: Comparable, b: Comparable): Comparable { // ... rest of the implementation }

We tell typescript - the getLongerValue function can be invoked with:

  • two objects and it will return an object
  • two arrays and it will return an array

The result is better type checking across the board, if we now hover over the longerArray variable we can see that it's of type string[].

And we also see an error at the invocation point where we assigned the longerMixed variable - the Error basically states:

Hey, you have specified that you either call the function with 2 elements of type string[] or 2 elements of type Record<string, string> - you're doing neither, therefore this behavior is not intended.

By using function overloads we can take advantage of the dynamic nature of the javascript language, while still using type checking.

In some cases a function has more than 1 signature for the type of its parameters and often times we know the return type of those functions at invoke time, based on the type of parameter we have passed in.

By overloading functions that take parameters of different types, we get type checked parameters at invoke time as well as typed return values.

Typescript checks for overload compatibility top to bottom. It checks the first overload, attempts to call the function with the provided parameters. If the types match, it picks that overload as the correct one, otherwise it proceeds to the next overload.

That's why we order our overloads from most specific to least specific.

Note that the function implementation is not part of the overload list, so our getLongerValue function only has 2 overloads. If we call our function with any parameter other than string[] or Record<string, string> we would get an error.

I wrote a book in which I share everything I know about how to become a better, more efficient programmer.
book cover
You can use the search field on my Home Page to filter through all of my articles.