
A Gentle Introduction
Often, one would start with a definition of function overloading, but let's start with a problem definition...
Consider calculating the area of a rectangle area = width * length
. For a circle, the formula is area = πr^2
. Now, write a function (area()
) that will calculate the area of either a rectangle or a circle.
1function areaOfRectangle(width: number, length: number) {
2 return width * length;
3}
4function areaOfCircle(radius: number) {
5 return Math.PI * (radius * radius);
6}
Nice try... but we want both functions to be called area()
.
Enter Function Overloading
Now the definition...
"Function overloading (or method overloading) is the ability to create multiple functions of the same name with different implementations. Calls to an overloaded function will run a specific implementation of that function appropriate to the context of the call, allowing one function call to perform different tasks depending on context." Wikipedia
In other words, we can create multiple functions with the same name but with different parameters (and even different return types).
To create a single function (`area()`) to handle both a rectangle and a circle, we start by defining the "signature" each individual one.
1function area(width: number, length: number): number; //rectangle
2function area(radius: number): number; //circle
Notice that these are just signatures; they do not include any implementation information. These overload signatures are what will appear in your intellisense.
console.log( area(3) );
>> function area(radius: number): number (+1 overload)
console.log( area(5,4) );
>> function area(width: number, length: number): number (+1 overload)
Once we have written the overload signatures, we need to create a general signature that will handle all of the possible parameters and provide the implementation.
1function area(a: number, b?: number) {
2 if (typeof(b) === 'number') {
3 //we have a rectangle...
4 return a * b;
5 }
6 //else, we have a circle...
7 return Math.PI * (a*a);
8}
Why is the second parameter optional?
When we are calculating the area of a rectangle, we need two parameters, but when we are working with a circle, we only need one. Thus, sometimes area()
will have two arguments, and sometimes only one.
Review
There are two rules in function overloading:
- The same name is used each of the overloaded function
- The functions must have different type signatures, i.e. differ in the number or the types of their parameters and optionally in their return type.
A More Detailed Example
Let's consider a more involved example. We will start with a story...
The (Long) Setup
You have been tasked with creating a function that will format an age as a string for display. The function should take two arguments: years and months - both numbers.
1function displayAge(years: number, months: number) {
2 //convert to integers
3 years = Math.floor(years);
4 months = Math.floor(months);
5 //format
6 return `${years} years, ${months} months`;
7}
The team-lead loves your work, but DMs you the next day informing you that one API calculates age as a single real (float) number representing the years. She asks that you create a function for formatting age from a single numeric parameter. "No problem" is your response.
1function displayAgeFromNumber(age: number) {
2 const years = Math.floor(age);
3 const months = (age - years) * 12;
4 return displayAge(years, months);
5}
Again, your boss is amazed at your ability and sends you a coffee. The next day, she (sheepishly) lets you know that your fancy functions will be used in another project, but age is represented as a tuple or as properties in an object. "I am on it!" you reply.
1type AgeObject = {
2 years: number;
3 months: number;
4}
5function displayAgeFromObject(age: AgeObject) {
6 const {years, months} = age;
7 return displayAge(years, months);
8}
9
10type AgeTuple = [years: number, month: number];
11function displayAgeFromTuple(age: AgeTuple) {
12 const [years, months] = age;
13 return displayAge(years, months);
14}
While it takes your lead a bit more time to get back to you (she is not a TS pro like you), her enthusiasm for your work continues, and she sends you several entertaining animated GIFs. That evening while enjoying your kale and farro salad, you reflect on your day. Being an ever-dedicated, super-smart, star developer, you realize that it would make sense for there to be a function that formats age directly from a date. "I got this", you say to your pet cat, "Mr. Pickles".
1function displayAgeFromDOB(dob: Date) {
2 const ms = Date.now() - dob.getTime();
3 const age = ms / (
4 1000 //: 1000 ms/sec
5 * 60 //: 60 sec/min
6 * 60 //: 60 min/hr
7 * 24 //: 24 hr/day
8 * 365.25 //: 365-1/4 days/yr
9 );
10 return displayAgeFromNumber(age);
11}
The next day, you DM your lead to inform her of your new function to handle formatting age from a date. She is flabbergasted: "I was just going to ask you to do that!". She sends you bag of M&Ms (no peanuts - of course) and ask you to join her at the annual Typescript convention.
You are so excited and even remember to pack an extra bag for all of the swag you will get on the convention floor. The first session you and your lead attend is titled "Function Overloading in Typescript". By the end, you know exactly what your boss is going to ask of you!
Overloading Signature
When you get back to your desk, you pull out your notes from the Function Overloading session:
1//Start by defining the signatures of each overload
2function add(a: number, b: number): number;
3function add(a: string, b: string): string;
4function add(a: boolean, b: boolean): boolean;
5//General signature to handle all options
6function add(a: unknown, b: unknown) {
7 if (typeof(a) === 'number' && typeof(b) === 'number') {
8 return a + b;
9 }
10 else if (typeof(a) === 'string' && typeof(b) === 'string') {
11 return a + ' ' + b;
12 }
13 else if (typeof(a) === 'boolean' && typeof(b) === 'boolean') {
14 return a && b;
15 }
16
17 //else
18 return undefined; //or throw error?
19}
20
21const test1 = add(4, 5);
22// ^? const test1: number
23const test2 = add("hello", "world");
24// ^? const test2: string
25const test3 = add(true, false);
26// ^? const test3: boolean
In your notes, you have underlined and highlighted: "Start by defining the signatures of each overload..."
1function displayAge(years: number, months: number): string;
2function displayAge(age: number): string;
3function displayAge(age: AgeObject): string;
4function displayAge(age: AgeTuple): string;
5function displayAge(dob: Date): string;
The five possible parameter options are listed. "Be sure to include the return type for each of the functions!" you have written in the margins of your notes.
Implementation Signature
Now add the general signature (to handle all possible parameters) and implement the function!
1function displayAge(years: number, months: number): string;
2function displayAge(age: number): string;
3function displayAge(age: AgeObject): string;
4function displayAge(age: AgeTuple): string;
5function displayAge(dob: Date): string;
6function displayAge(a: unknown, b?: unknown) {
7 if (typeof(a) === 'number' && typeof(b) === 'number') {
8 //convert to integers
9 a = Math.floor(a);
10 b = Math.floor(b);
11 //format
12 return `${a} years, ${b} months`;
13 }
14 else if (typeof(a) === 'number') {
15 const years = Math.floor(a);
16 const months = (a - years) * 12;
17 return displayAge(years, months);
18 }
19 else if (a instanceof Date) {
20 const ms = Date.now() - a.getTime();
21 const age = ms / (
22 1000 //: 1000 ms/sec
23 * 60 //: 60 sec/min
24 * 60 //: 60 min/hr
25 * 24 //: 24 hr/day
26 * 365.25 //: 365-1/4 days/yr
27 );
28 return displayAge(age);
29 }
30 else if ('years' in (a as AgeObject) && 'months' in (a as AgeObject)) {
31 const { years, months } = (a as AgeObject);
32 return displayAge(years, months);
33 }
34 else if (Array.isArray(a) && a.length === 2) {
35 const [ years, months ] = a;
36 return displayAge(years, months);
37 }
38
39 //else - not handled
40 return "";
41}
You have done it! You commit your code and wait for you lead to heap the (well deserved) praises on you. Instead, you get an email from HR letting you know that your team-lead has left to pursue her love of Urban Farming, and they have replaced the team-lead position with someone from Microsoft named Clippy.
What About Classes?
So far, we have focused on overloading functions; but we can overload methods in classes the same way.
Method Overloading
Consider a calendar for students to track their lives. Students would want to be able to add homework assignments, be reminded of exam dates, and (of course) track parties.
1type CalendarEvent = {
2 kind: 'Assignment' | 'Exam' | 'Party',
3 date: Date,
4 text: string;
5 data?: string;
6}
7class StudentCalendar {
8 private readonly events: CalendarEvent[] = [];
9
10 add(event: CalendarEvent) { /* ... */ }
11}
Remembering students can be lazy, you decide to include the ability to pass in an Assignment, Exam, or Party into our `add()` method. Easy!
1class StudentCalendar {
2 private readonly events: CalendarEvent[] = [];
3
4 add(event: CalendarEvent | Assignment | Exam | Party) {
5 /* ... */
6 }
7}
This will work, but you are a TS Pro and have studied function overloading. So you decide to implement add()
using method overloading.
1class StudentCalendar {
2 private readonly events: CalendarEvent[] = [];
3
4 add(event: CalendarEvent): void;
5 add(assignment: Assignment): void;
6 add(exam: Exam): void;
7 add(party: Party): void;
8 add(data: CalendarEvent | Assignment | Exam | Party) {
9 if ('text' in data) {
10 //data is a CalendarEvent
11 this.events.push(data);
12 }
13 else {
14 //data is Assignment | Exam | Party
15 //so we need to create the CalendarEvent
16 const event: CalendarEvent = {
17 kind: data.kind,
18 date: data.kind === 'Assignment' ? data.due : data.date,
19 text: data.kind === 'Assignment'
20 ? data.description
21 : data.kind === 'Exam'
22 ? data.topic
23 : data.location,
24 data: 'course' in data ? data.course : undefined,
25 };
26 this.events.push(event);
27 }
28 }
29}
Well done. Notice that the details of the implementation are the same with or without method overloading, but by adding the method signatures, you will now get nice intellisense.
View the full code (including type definitions) in this Playground
Constructor Overloading
Continuing with our StudentCalendar
, let's make it so you can optionally instantiate the class with a single event or set of events. And, yes, you are a TS Pro so constructor overloading is your choice of implementation.
1class StudentCalendar {
2 private readonly events: CalendarEvent[];
3
4 constructor();
5 constructor(event: CalendarEvent);
6 constructor(events: CalendarEvent[]);
7 constructor(data?: CalendarEvent | CalendarEvent[]) {
8 if (data) {
9 this.events = Array.isArray(data)
10 ? data.map(evt => { return {...evt}; })
11 : [{...data}]
12 }
13 else {
14 this.events = [];
15 }
16 }
17}
Why Overload?
We could have gotten away with not overloading the functions/methods/constructors presented in this article. In fact, overloading is just providing adding the overload signatures. Without them, the function (or class constructor/method) work just fine.
So why should we go through the extra work of adding overload signatures?
First - you will get better intellisense when you use function overloading. Have a look at the intellisense of displayAge()
with and without overloading.
1// without overloading
2function displayAge(a: unknown, b?: unknown): string
3
4// with overloading (scrolling through each)
5function displayAge(years: number, months: number): string (+4 overloads)
6function displayAge(age: number): string (+4 overloads)
7function displayAge(age: AgeObject): string (+4 overloads)
8function displayAge(age: AgeTuple): string (+4 overloads)
9function displayAge(dob: Date): string (+4 overloads)
Clearly, overloading gives you more detail on the argument(s) for the function.
Second - overloading better communicates your intent - what you are trying to do with this set of code. The rewards better communication of intent are twofold. As you write your code, overloading allows you to articulate (in your mind and in code) exactly what you are trying to accomplish: i.e. how this function will be used. Additionally, better communication of intent will help you and others maintain the code in the future; the more we know about the original intent of a piece of code the easier it is to keep the code up-to-date.