What is function overloading?

Jul 17, 2024//-5 min

Function overloading is a feature in many programming languages that allows the creation of multiple functions with the same name but different parameters. This means you can define several functions with identical names, differing only in the number or type of parameters. This technique is beneficial for improving code readability, maintainability, and flexibility.

Function overloading in OOP languages

In OOP languages like Java and C++, function overloading is a fundamental concept. It allows methods to be defined with the same name but different parameter lists. The correct method is selected at compile time based on the method signature, which includes the method name and parameter types.

Java example

In Java it is implemented in the following syntax:

1class Notification {
2  void send(String email, String message) {
3    // ...
4  }
5
6  void send(String email, String message, boolean isUrgent) {
7    // ...
8  }
9
10  void send(String email, String subject, String body) {
11    // ...
12  }
13}
14
15public class Main {
16  public static void main(String[] args) {
17    Notification notifier = new Notification();
18
19    notifier.send("Hello!");
20    notifier.send(
21      "Reminder",
22      "The meeting time has been changed to 10 AM."
23    );
24    notifier.send(
25      "ensbaspinar@gmail.com",
26      "Can you send me the plans for the team?",
27      true
28    );
29  }
30}

C++ example

Also C++ has a similar syntax:

1class Notification {
2public:
3  void send(const string& message) {
4    // ...
5  }
6
7  void send(const string& subject, const string& body) {
8    // ...
9  }
10
11  void send(const string& email, const string& message, bool isUrgent) {
12    // ...
13  }
14};
15
16int main() {
17  Notification notifier;
18
19  notifier.send("Hello!");
20  notifier.send(
21    "Reminder",
22    "The meeting time has been changed to 10 AM."
23  );
24  notifier.send(
25    "ensbaspinar@gmail.com",
26    "Can you send me the plans for the team?",
27    true
28  );
29}

In both Java and C++, the overload resolution happens at compile time, ensuring that the appropriate method is invoked based on the arguments provided.

How to handles function overloading?

  1. Function Signatures
    The compiler checks the signatures of functions. When multiple functions with the same name are defined, it checks whether the parameter lists of these functions are different. Unless there is a signature difference, a compilation error occurs.
  2. Name Mangling
    The compiler uses the name mangling technique to distinguish functions with the same name. With this technique, function names are made unique by encoding them together with their parameter types and numbers. For example,
    foo(int)
    and
    foo(double)
    are represented by the compiler with different names such as
    foo_i
    and
    foo_d
    .
  3. Call Resolution
    When a function is called, the compiler checks the types of the arguments to resolve which function the call corresponds to. A set of rules and precedence ordering is used to find the most appropriate function signature. If the compiler cannot find a match, or if there is more than one match, it will return a compilation error.

Function overloading in TypeScript

TypeScript, being a statically typed superset of JavaScript, also supports function overloading but in a slightly different manner compared to traditional OOP languages. In TypeScript, function overloading is accomplished through the use of overload signatures and a single implementation.

Example

1class Notification {
2  send(message: string): void;
3  send(subject: string, body: string): void;
4  send(email: string, message: string, isUrgent?: boolean): void;
5
6  send(arg1: unknown, arg2?: unknown, arg3?: unknown): void {
7    if (typeof arg1 === "string" && !arg2 && !arg3) {
8      // ...
9    } else if (typeof arg1 === "string" && typeof arg2 === "string" && !arg3) {
10      // ...
11    } else if (typeof arg1 === "string" && typeof arg2 === "string" && typeof arg3 === "boolean") {
12      // ...
13    }
14  }
15}
16
17const notifier = new Notification();
18
19notifier.send("Hello!");
20notifier.send(
21  "Reminder",
22  "The meeting time has been changed to 10 AM."
23);
24notifier.send(
25  "ensbaspinar@gmail.com",
26  "Can you send me the plans for the team?",
27  true
28);

Example 2

1type Callback = () => void;
2
3export function useSeenObserver(callback: Callback): React.RefObject<HTMLElement>;
4export function useSeenObserver(selector: string, callback: Callback): void;
5export function useSeenObserver(arg1: string | Callback, arg2?: Callback): React.RefObject<HTMLElement> | void {
6  let element: Element;
7  let callback: Callback;
8  let ref = useRef<HTMLElement>(null);
9
10  if (typeof arg1 === "string") {
11    element = document.querySelector(arg1)!;
12    callback = arg2!;
13  } else {
14    element = ref.current!;
15    callback = arg1!;
16  }
17
18  // ...
19
20  if (typeof arg1 !== "string") {
21    return ref;
22  }
23}
24
25/* Usage 1 */
26const ref = useSeenObserver(() => {
27  // ...
28});
29
30/* Usage 2 */
31useSeenObserver("#component", () => {
32  // ...
33});

How to handles function overloading?

  1. Checking Function Signatures
    Typescript checks the type at build-time to see if your function is compatible with one of the specified signatures. If not, it gives a type error.
  2. Call Resolution
    Typescript is compiled into JavaScript at build-time. Therefore, at run-time, TypeScript's type safety and signature checks do not apply at run-time. The function itself checks parameter types with in-code checks.

Why should I use it?

You may wonder why I should use signature instead of union in Typescript. Let's rewrite the last example without using signature.

1type Callback = () => void;
2
3export function useSeenObserver(arg1: string | Callback, arg2?: Callback): React.RefObject<HTMLElement> | void {
4  let element: Element;
5  let callback: Callback;
6  let ref = useRef<HTMLElement>(null);
7
8  if (typeof arg1 === "string") {
9    element = document.querySelector(arg1)!;
10    callback = arg2!;
11  } else {
12    element = ref.current!;
13    callback = arg1!;
14  }
15
16  // ...
17
18  if (typeof arg1 !== "string") {
19    return ref;
20  }
21}

Next, let's look at the type TypeScript suggests and the check it does when we use the

useSeenObserver
hook.

As you can see, it cannot give a clear function overloading type. Argument names are meaningless. It is not clear which parameter should be used with which. TypeScript does not give an error for following variants:

  • (arg1: string)
    -> incorrect use
  • (arg1: string, arg2: Callback)
  • (arg1: Callback)
  • (arg1: Callback, arg2: Callback)
    -> incorrect use

When you use signature, you get a correct API as below.