# TypeScript: The humble function overload

With the most recent type system features like conditional types or variadic tuple types, one technique to describe a function’s interface has faded into the background: Function overloads. And there’s a good reason for that. Both features have been implemented to deal with the shortcomings of regular function overloads.

See this concatenation example directly from the TypeScript 4.0 release notes. This is an array `concat`

function:

`function concat(arr1, arr2) {`

return [...arr1, ...arr2];

}

To correctly type a function like this so it takes all possible edge cases into account, we would end up in a sea of overloads:

`// 7 overloads for an empty second array`

function concat(arr1: [], arr2: []): [];

function concat<A>(arr1: [A], arr2: []): [A];

function concat<A, B>(arr1: [A, B], arr2: []): [A, B];

function concat<A, B, C>(arr1: [A, B, C], arr2: []): [A, B, C];

function concat<A, B, C, D>(arr1: [A, B, C, D], arr2: []): [A, B, C, D];

function concat<A, B, C, D, E>(arr1: [A, B, C, D, E], arr2: []): [A, B, C, D, E];

function concat<A, B, C, D, E, F>(arr1: [A, B, C, D, E, F], arr2: []): [A, B, C, D, E, F];)

// 7 more for arr2 having one element

function concat<A2>(arr1: [], arr2: [A2]): [A2];

function concat<A1, A2>(arr1: [A1], arr2: [A2]): [A1, A2];

function concat<A1, B1, A2>(arr1: [A1, B1], arr2: [A2]): [A1, B1, A2];

function concat<A1, B1, C1, A2>(arr1: [A1, B1, C1], arr2: [A2]): [A1, B1, C1, A2];

function concat<A1, B1, C1, D1, A2>(arr1: [A1, B1, C1, D1], arr2: [A2]): [A1, B1, C1, D1, A2];

function concat<A1, B1, C1, D1, E1, A2>(arr1: [A1, B1, C1, D1, E1], arr2: [A2]): [A1, B1, C1, D1, E1, A2];

function concat<A1, B1, C1, D1, E1, F1, A2>(arr1: [A1, B1, C1, D1, E1, F1], arr2: [A2]): [A1, B1, C1, D1, E1, F1, A2];

// and so on, and so forth

And this only takes into account arrays that have up to six elements. Variadic tuple types help greatly with situations like this:

`type Arr = readonly any[];`

function concat<T extends Arr, U extends Arr>(arr1: T, arr2: U): [...T, ...U] {

return [...arr1, ...arr2];

}

You can easily see how boils down the function signature to its point while being flexible enough for all possible arrays to come. The return value also maps to the return type. No extra assertions, TypeScript can make sure that you are returning the correct value.

It’s a similar situation with conditional types. This example comes directly from my book. Think of software that retrieves orders based on customer, article, or order ID. You might want to create something like this:

`function fetchOrder(customer: Customer): Order[]`

function fetchOrder(product: Product): Order[]

function fetchOrder(orderId: number): Order

// the implementation

function fetchOrder(param: any): Order | Order[] {

//...

}

But this is just half the truth. What if you end up with ambiguous types where you don’t know exactly if you get *only* a Customer, or only a *Product*. You need to take care of all possible combinations:

`function fetchOrder(customer: Customer): Order[]`

function fetchOrder(product: Product): Order[]

function fetchOrder(orderId: number): Order

function fetchOrder(orderId: Customer | Product): Order[]

function fetchOrder(orderId: Customer | number): Order | Order[]

function fetchOrder(orderId: number | Product): Order | Order[]

// the implementation

function fetchOrder(param: any): Order | Order[] {

//...

}

Add more possibilities, you end up with more combinations. Here, conditional types can reduce your function signature tremendously.

`type FetchParams = number | Customer | Product;`

type FetchReturn<T> = T extends Customer ? Order[] :

T extends Product ? Order[] :

T extends number ? Order: never

function fetchOrder<T extends FetchParams>(params: T): FetchReturn<T> {

//...

}

Since conditional types distribute a union, `FetchReturn`

returns a union of return types.

So, there is good reason to use those techniques instead of drowning in too many function overloads. This bears the question: Do we still need function overloads?

TL;DR: Yes, we need function overloads.

Here are a few examples.

## Different function shapes #

One scenario where function overloads still are very handy is if you have different argument lists for your function variants. This means that not only the arguments (parameters) themselves can have some variety (this is where conditionals and variadic tuples are fantastic), but also the number and position of arguments.

Imagine a search function that has two different ways of being called:

- Call it with the search query. It returns a
*Promise*you can await. - Call it with the search query and a callback. In this scenario, the function does not return anything.

This *can* be done with conditional types, but is very unwieldy:

// => (1)

type SearchArguments =

// Argument list one: a query and a callback

[query: string, callback: (results: unknown[]) => void] |

// Argument list two:: just a query

[query: string];

// A conditional type picking either void or a Promise depending

// on the input => (2)

type ReturnSearch<T> = T extends [query: string] ? Promise<Array<unknown>> : void;

// the actual function => (3)

declare function search<T extends SearchArguments>(...args: T): ReturnSearch<T>;

// z is void

const z = search("omikron", (res) => {

})

// y is Promise<unknown>

const y = search("omikron")

Here’s what we did:

- We defined our argument list using tuple types. Since TypeScript 4.0, we can name tuple fields just like we would do it with objects. We create a union because we have two different variants of our function signature
- The
`ReturnSearch`

type selects the return type based on the argument list variant. If it’s just a string, return a Promise, if it has a callback, return void. - We add our types by constraining a generic variable to
`SearchArguments`

, so that we can correctly select the return type

That is a lot! And it features a ton of complex features that we love to see in TypeScript’s feature list: Conditional types, generics, generic constraints, tuple types, union types! We get *some* nice auto-complete, but it’s nowhere the clarity of a simple function overload:

function search(query: string): Promise<unknown[]>

function search(query: string, callback: (result: unknown[]) => void): void

// This is the implementation, it only concerns you

function search(query: string, callback?: (result: unknown[]) => void): void | Promise<unknown> {

// Implmeent

}

We only use a union type for the implementation part. The rest is very explicit and clear. We know our arguments, we know what to expect in return. No ceremony, just simple types. The best part of function overloads is that the *actual* implementation does not pollute the type space. You can go for a round of *any*s and just don’t care.

## Exact arguments #

Another situation where function overloads can make a lot of things easier is when you are in need of exact arguments and their mapping. Let’s look at a function that applies an event to an event handler. E.g. we have a `MouseEvent`

and want to call a `MouseEventHandler`

with it. Same for keyboard events, etc. If we use conditionals and union types to map event and handler, we might end up with something like this:

`// All the possible event handlers`

type Handler =

MouseEventHandler<HTMLButtonElement> |

KeyboardEventHandler<HTMLButtonElement>;

// Map Handler to Event

type Ev<T> =

T extends MouseEventHandler<infer R> ? MouseEvent<R> :

T extends KeyboardEventHandler<infer R> ? KeyboardEvent<R> : never;

// Create a

function apply<T extends Handler>(handler: T, ev: Ev<T>): void {

handler(ev as any); // We need the assertion here

}

At a first glance, this looks fine. It might be a bit cumbersome though if you think about all the variants that you need to keep track of.

There’s a bigger problem, though. The way TypeScript deals with all possible variants of event is causing an *unexpected intersection*. This means that in the function body, TypeScript can’t tell what kind of handler you are passing. Therefore it also can’t tell which kind of event we’re getting. So TypeScript says that the event can be both. A mouse event, and a keyboard event. You need to pass handlers that can deal with both. Which is not how we intend our function to work.

The actual error message is *TS 2345: Argument of type ‘KeyboardEvent | MouseEvent<HTMLButtonElement, MouseEvent>’ is not assignable to parameter of type ‘MouseEvent<HTMLButtonElement, MouseEvent> & KeyboardEvent’.*

This is why we need an `as any`

type assertion. Just to make it possible to actually call the handler with the event.

So, the function signature works in a lot of scenarios:

`declare const mouseHandler: MouseEventHandler<HTMLButtonElement>;`

declare const mouseEv: MouseEvent<HTMLButtonElement>

declare const keyboardHandler: KeyboardEventHandler<HTMLButtonElement>;

declare const keyboardEv: KeyboardEvent<HTMLButtonElement>;

apply(mouseHandler, mouseEv); // yeah!

apply(keyboardHandler, keyboardEv) // cool!

apply(mouseHandler, keyboardEv) // 💥breaks like it should!

But once there’s ambiguity, stuff doesn’t work out as it should:

`declare const mouseOrKeyboardHandler:`

MouseEventHandler<HTMLButtonElement> |

KeyboardEventHandler<HTMLButtonElement>;;

// No wait, this can cause problems!

apply(mouseOrKeyboardHandler, mouseEv);

When `mouseOrKeyboardHandler`

is a keyboard handler, we can’t reasonably pass a mouse event. Wait a second. This is exactly what the **TS2345** error from above tried to tell us! We just shifted the problem to another place and made it silent with an *as any* assertion. Oh no!

Explicit, exact function signatures make *everything* easier. The mapping becomes clearer, the type signatures easier to understand, and there’s no need for conditionals or unions.

`// Overload 1: MouseEventHandler and MouseEvent`

function apply(

handler: MouseEventHandler<HTMLButtonElement>,

ev: MouseEvent<HTMLButtonElement>): void

// Overload 2: KeyboardEventHandler and KeyboardEvent

function apply(

handler: KeyboardEventHandler<HTMLButtonElement>,

ev: KeyboardEvent<HTMLButtonElement>): void

// The implementation. Fall back to any. This is not a type!

// TypeScript won't check for this line nor

// will it show in the autocomplete.

//This is just for you to implement your stuff.

function apply(handler: any, ev: any): void {

handler(ev);

}

Function overloads help us with all possible scenarios. We basically make sure that there no ambiguous types:

`apply(mouseHandler, mouseEv); // yeah!`

apply(keyboardHandler, keyboardEv) // cool!

apply(mouseHandler, keyboardEv) // 💥 breaks like it should!

apply(mouseOrKeyboardHandler, mouseEv); // 💥 breaks like it should

For the implementation, we can even use *any*. This is not a type seen by TypeScript, this is just for you to implement your stuff. Since you can make sure that you won’t run into a situation that implies ambiguity, we can rely on the happy-go-lucky type and don’t need to bother.

## Bottom line #

Function overloads are still very useful and for a lot of scenarios the way to go. They’re easier to read, easier to write, and in a lot of cases more exact than what we get with other means.

But it’s not either-or. You can happily mix and match conditionals and function overloads if your scenario needs it. As always, here are some playgrounds: