DEV Community

Cover image for Function Overloading in Typescript
thomas for This is Angular

Posted on • Updated on • Originally published at Medium

Function Overloading in Typescript

Typescript is a type system built on top of Javascript and its only purpose is to secure your application by applying strong type security.

Thanks to Typescript, you can reduce runtime errors by catching them at build time. As a good teammate, it's helpful to create strong-typed functions when creating utilities functions. By doing so, auto-completion and type inference will assist your teammates when using your function, and it will reduce errors.

Let's see how we can achieve that by looking at a technique called function overloading.


In this example, we aim to create a function with the following capabilities:

  • Filter an array of strings by checking if each element starts with a specified search parameter. 
  • Choose to return either the first match or all elements that matched. 
  • Check if a given string starts with the search parameter

The function below demonstrates how to implement these conditions:

function select(arr: string[] | string, search: string, firstOnly = false){
        // ^? string | boolean | string[] | undefined
  if(Array.isArray(arr)){
    return firstOnly 
            ? arr.find(a => a.startsWith(search))
            : arr.filter(a => a.startsWith(search))
  }else {
    return arr.startsWith(search);
  }
}
Enter fullscreen mode Exit fullscreen mode

Note: The implementation details are not the focus of this article.

Now, let's call this fonction to return all elements that match the search parameter:

const names = ['toto', 'jack', 'robert'];
const t = filter(names, 'to');
//    ^? string | boolean | string[] | undefined
Enter fullscreen mode Exit fullscreen mode

Issue: The inferred return type is string | boolean | string[] | undefined. Typescript is not able to narrow down the return type to string[], even though we know that's the only possible return type.

typescript cannot narrow type

In this second example, if we want to use the function for checking if the given string matches the search parameter, Typescript allows us to set the firstOnly parameter which is useless and confusing in our case.


To address these issues, we can overload our filter function. Overloading involves defining multiple versions of the same function with different parameter types and return types.

In this example, we define three different versions of the filter function:

function filter(arr:string[], search: string): string[];
function filter(arr:string[], search: string, firstOnly: true): string | undefined;
function filter(elt:string, search: string): boolean;

function filter(arr: string[] | string, search: string, firstOnly = false){
  if(Array.isArray(arr)){
    return firstOnly ? arr.find(a => a.startsWith(search)): arr.filter(a => a.startsWith(search))
  }else {
    return arr.startsWith(search);
  }
}
Enter fullscreen mode Exit fullscreen mode

The first version takes an array of strings and a search parameter and returns an array of matching strings. The second version adds a firstOnly parameter which, when set to true, returns only the first matching string or undefined if no match is found. The third version takes a single string and a search parameter and returns a boolean value indicating whether the string starts with the search parameter.

The implementation of the filter function then concatenates all the definitions into one. However, this combined definition cannot be used directly in the code. If we want to call the filter function with a string as the first parameter and use the boolean as the last parameter, we need to add a new function definition.

Overloading the filter function has several benefits:

  • The firstOnly parameter can only be set when the first parameter is an array.
  • The inferred type is correctly narrowed depending on the function's parameters.

Examples:

error thanks to auto-completion

The image shows an error thrown by Typescript because we have set the firstOnly parameter but our first argument is a string and no function definition matches this implementation.

const names = ['toto', 'jack', 'robert'];
const first = filter(names, 'to', true);
//    ^? string | undefined

const second = filter(names, 'to');
//    ^? string[]

const third = filter('toto', 'to')
//    ^? boolean
Enter fullscreen mode Exit fullscreen mode

The function calls get a nice auto-completion and type inference making working with the filter function easier.

Warning: However there is still a significant warning to consider.

While calling the function has become much safer, we have lost all type safety inside the implementation. By telling TypeScript that we know the types better than it does, TypeScript will not protect us if our definitions are incorrect.

Let's illustrate this warning with an example of how we could deceive our team members by lying about the function's behavior:

function lying(elt:string): number;
function lying(elt:number): string;

function lying(elt: string | number){
  return elt;
}
Enter fullscreen mode Exit fullscreen mode
const t = lying('toto');
//    ^? number
Enter fullscreen mode Exit fullscreen mode

In the above code, the function is simple, but it demonstrates the problem perfectly. We have set two definitions that TypeScript will trust, but as we can see, the return type is incorrect. When we call the function, the inferred type of t is incorrect.

While function overloading can be incredibly helpful, it's essential to be cautious and ensure that the types are as accurate as possible. Sometimes, it's better to have less accurate types than to lie to our users.


I hope I have helped you learn about function overloading in Typescript! It's definitely an advanced feature, but it can be very useful in making your code safer and more reliable. Remember to be cautious when using function overloading, as it can lead to type safety issues if not used carefully.

Good luck with your Typescript projects, and don't hesitate to reach out to me if you have any more questions! You can find me on Twitter or Github.

Top comments (16)

Collapse
 
mtrantalainen profile image
Mikko Rantalainen

I think without real support for defining implementations of overloaded functions, you shouldn't overload functions at all. It's much better for maintainability to create different names for different things instead of overloading the same name for totally different signatures.

Collapse
 
patricknelson profile image
Patrick Nelson • Edited

Yeah, that's what I liked about C# (first class support of overloading). That is: If you have a different signature, then you will also have code in your method or function body that corresponds directly it. To me, I always found that easier to comprehend.

That's why I might still prefer using a union type here, i.e. string[] | string where you join the types together with |, and just ensure that whatever you define is generally compatible with that. You still have to implement some logic, but it's probably a little simpler than if you were to change or omit middle parameters. I just found it easier to comprehend when reading it, especially if it's someone else's code.

I know it's very broadly speaking, so that's just my opinion and I understand each use case may vary. 😅 It's cool to see this is possible, though.

Collapse
 
achtlos profile image
thomas

For some use case, I agree with you but for other, I don't want to create multiple functions.

My exemple is maybe poorly chosen but it gives the idea of what function overloading can archive.

Collapse
 
mtrantalainen profile image
Mikko Rantalainen

Yes, I use function overloading with languages that actually support it, such as C++. However, the "supported in signature but not for implementation" doesn't cut it.

Thread Thread
 
achtlos profile image
thomas

Yes I agree with you. Coming from java, it was hard at first to understand this. But for lib author, that's very neat.

Collapse
 
efpage profile image
Eckehard

Function overloading is definitive a feature I am missing in JS. Thank you for your post!

Collapse
 
voxpelli profile image
Pelle Wessman

It is actually now possible in JS using JSDoc together with the new TS 5.0: dev.to/voxpelli/typescript-50-for-...

Collapse
 
awaisalwaisy profile image
Alwaisy al-waisy

I learnt a lot of new concepts in this article.

Collapse
 
achtlos profile image
thomas

Thanks for mentioning it. That's great to hear 🙏

Collapse
 
aazad1234 profile image
aazad1234

It has good explanation with example, thank you for the knowledge

Collapse
 
raibtoffoletto profile image
Raí B. Toffoletto

If you are dealing with classes, overloading the constructor is quite helpful. But for pure functions I rather let TS define them and then validate the result myself or use "as" if I'm confident of the outcome. This will be JS after all.

Collapse
 
achtlos profile image
thomas

of course everything will be JS at the end. But TS is here to help. When creating library, it's always nice to have autocomplete. My exemple is very simple, but sometimes, functions are more complicated.

And for the return type, I prefer to have the correct return type than using the "as" keyword. When everything is your own implementation, why not. But when working on teams or npm lib, function overload is an awesome feature.

Collapse
 
synthetic_rain profile image
Joshua Newell Diehl

One should be wary of the performance costs of this kind of strategy. While I understand that performance is not often a primary concern of front-end web development, this level of overloading does beg the question of how many additional stubs must be generated by the JIT compiler in order to make this kind of type flexibility possible.

Collapse
 
achtlos profile image
thomas

you mean performance at compile time? since all types are removed at runtime. I don't think that a good argument not to use them.

Collapse
 
synthetic_rain profile image
Joshua Newell Diehl

No, not performance at typescript compilation.
The V8 JIT must consume more memory to support polymorphic functions because stub signatures are generated to account for all type possibilities.

Then if the arguments received at runtime change too frequently, the TurboFan optimizing compiler may fail to heat up your operations.

I didn't mean to suggest that polymorphism by way of overloading should be totally avoided. Just a friendly "Here may be dragons."

Thread Thread
 
achtlos profile image
thomas

Ok thanks for the explanation. Didn't know that.