Function overloading is an OOP feature. It lets you define multiple functions with the same name, but different implementations. TypeScript has semi-support for function overloading; it allows you to to define multiple signatures for the same function, but only one implementation.
This can still be useful, however, and in this article, I’ll explain how to use function overloading, and why.
What’s The Problem?
Let’s take an example function. Here’s a function to calculate the area of a rectangle or a square, depending on what parameters we give it:
function area(baseOrSize: number, height?: number) {
if (height) return baseOrSize * height;
return baseOrSize * baseOrSize;
}
This function works fine without the extra signatures, so what’s the benefit of function overloading?
The main benefit is clarity. With function overloading, I can define multiple signatures for this function:
function area(size: number): number;
function area(base: number, height: number): number;
Then, TypeScript can intelligently narrow down which one I’m trying to use. We also get some helpful suggestions for all the possible signatures:
More Examples
Here’s another example using the common concat function, which merges two arrays if given them, or adds two strings together. For a more in-depth look at merging arrays, check out this article.
function concat<T>(
a: string | T[],
b: string | T[]
): string | T[] | undefined {
if (typeof a === 'string' && typeof b === 'string') return a + b;
if (Array.isArray(a) && Array.isArray(b)) return [...a, ...b];
}
If we get in two strings, we get out a string. If we give in two arrays, we get out an array. At first glance though, this function looks confusing. There are two possibilities for the parameters and the return type, and we even have a generic thrown in.
We also get no type inference on this:
We know we’re passing in two strings, so we should get out a string, but TypeScript doesn’t. We can even mix and match and do something like:
concat('foo', [7]);
Which we don’t want to be possible.
With function overloading, we can separate out and define both methods we have merged together here:
function concat(stringA: string, stringB: string): string;
function concat<T>(arrayA: T[], arrayB: T[]): T[];
And with two magic lines, TypeScript is able to infer your return type and which version of the method you’re calling.
concat([1], [2]); //returns number[]
concat('foo', 'bar'); //returns string
concat('foo', [7]); //❌ TS2769: No overload matches this call.
And since there’s no function signature where we’re mixing strings and arrays, TypeScript no longer allows it.
Again, we also get some helpful IDE hints:
Our code worked without these signatures, but by adding them we get some really helpful hints. We’ve defined how we’re going to use this function. Thanks to that, TypeScript is able to tell us when we’re using it right, what happens when we use it with different parameters, and when we’re using it wrong.
Pitfalls
In my functions, you might be wondering why I’ve used type unions and optional parameters to combine all my logic into a single method. Let’s take our previous example then, and see why. I’ll separate out my logic into a separate function for arrays and a separate function for strings.
There’s an issue here, however. TypeScript lets you define multiple signatures for the same method, but not multiple implementations. This is because TypeScript intentionally doesn’t affect anything at runtime, to be completely compatible with JavaScript. Since JavaScript doesn’t let you overload function implementations, TypeScript doesn’t either. This means function overloading in TypeScript is more signature overloading, rather than overloading the implementation.
Conclusion
Thanks for reading! TypeScript function overloading is a great technique when you want to redefine method signatures for different parameters, especially when you have a variable number of parameters. If you liked this article, or if you had any troubles following along, feel free to leave a comment below!
💬 Leave a comment