What is function overloading?
Function overloading is the process of redefining a function with the same name but different parameter combinations. The compiler or type system determines the correct function to call based on the parameters.
Function overloading is the process of redefining a function with the same name but different parameter combinations. The compiler or type system determines the correct function to call based on the parameters.
Function overloading in OOP languages
Languages like Java and C++ directly support overloading. The compiler chooses the appropriate method based on the parameters passed.
In Java, the implementation looks like this:
class Notification { void send(String email, String message) { // ... }
void send(String email, String message, boolean isUrgent) { // ... }
void send(String email, String subject, String body) { // ... }}
public class Main { public static void main(String[] args) { Notification notifier = new Notification();
notifier.send("Hello!"); notifier.send( "Reminder", "The meeting time has been changed to 10 AM." ); notifier.send( "ensbaspinar@gmail.com", "Can you send me the plans for the team?", true ); }}
A similar implementation can be seen in C++:
class Notification {public: void send(const string& message) { // ... }
void send(const string& subject, const string& body) { // ... }
void send(const string& email, const string& message, bool isUrgent) { // ... }};
int main() { Notification notifier;
notifier.send("Hello!"); notifier.send( "Reminder", "The meeting time has been changed to 10 AM." ); notifier.send( "ensbaspinar@gmail.com", "Can you send me the plans for the team?", true );}
In these examples, send
method can be called with different numbers and types of parameters.
How does function overloading work?
- The compiler checks the signatures of functions with the same name. If their parameter lists are different, compilation succeeds; otherwise, a compilation error occurs.
- It uses a technique called name mangling to distinguish functions with the same name. This technique encodes function names with parameter count and types to make them unique. For example,
foo(int, double)
andfoo(double, double)
might be represented internally asfoo_id
andfoo_dd
. - When the function is called, the compiler checks the argument types and matches them to the correct function.
Function overloading in TypeScript
TypeScript does not support overloading in the same way as OOP languages. Instead, it allows you to define multiple type signatures for a single implementation.
Example
class Notification { send(message: string): void; send(subject: string, body: string): void; send(email: string, message: string, isUrgent?: boolean): void; send(arg1: unknown, arg2?: unknown, arg3?: unknown): void { // If signature is send(message: string) if (typeof arg1 === "string" && !arg2 && !arg3) { // ... }
// If signature is send(subject: string, body: string) else if (typeof arg1 === "string" && typeof arg2 === "string" && !arg3) { // ... }
// If signature is send(email: string, message: string, isUrgent?: boolean) else if (typeof arg1 === "string" && typeof arg2 === "string" && typeof arg3 === "boolean") { // ... } }}
const notifier = new Notification();
notifier.send("Hello!");notifier.send("Hello!");notifier.send( "Reminder", "The meeting time has been changed to 10 AM.");notifier.send( "ensbaspinar@gmail.com", "Can you send me the plans for the team?", true);
Example 2
Let me also share a hook I wrote recently at work that motivated this blog post.
type Callback = () => void;
export function useSeenObserver(callback: Callback): React.RefObject<HTMLElement>;export function useSeenObserver(selector: string, callback: Callback): void;export function useSeenObserver(arg1: string | Callback, arg2?: Callback): React.RefObject<HTMLElement> | void { let element: Element; let callback: Callback; let ref = useRef<HTMLElement>(null);
if (typeof arg1 === "string") { element = document.querySelector(arg1)!; callback = arg2!; } else { element = ref.current!; callback = arg1!; }
// ...
if (typeof arg1 !== "string") { return ref; }}
When using the hook, you can pass a ref to the element like this:
const ref = useSeenObserver(() => { // ...});
return <div ref={ref}></div>;
Or you can use a selector as shown below:
useSeenObserver("#component", () => { // ...});
return <div id="component"></div>;
How does function overloading work?
- During development and compilation, TypeScript performs type checks to ensure the function matches one of the declared signatures.
- Since TypeScript compiles to JavaScript, type safety and signature checks do not run at runtime. Therefore, it is necessary to check parameter types manually within the function.
Why should I use it?
You might wonder why you should use signatures when it is possible to define the function with a union type. Let’s rewrite the last example without multiple signatures:
type Callback = () => void;
export function useSeenObserver(arg1: string | Callback, arg2?: Callback): React.RefObject<HTMLElement> | void { let element: Element; let callback: Callback; let ref = useRef<HTMLElement>(null);
if (typeof arg1 === "string") { element = document.querySelector(arg1)!; callback = arg2!; } else { element = ref.current!; callback = arg1!; }
// ...
if (typeof arg1 !== "string") { return ref; }}
When using this implementation, here’s what TypeScript’s autocomplete and type checking looks like:
As you can see, it cannot clearly show a valid template. The argument names and their valid combinations become unclear. TypeScript won’t throw an error in the following cases:
-
(arg1: string)
-> invalid usage -
(arg1: string, arg2: Callback)
-> valid usage -
(arg1: Callback)
-> valid usage -
(arg1: Callback, arg2: Callback)
-> invalid usage
When you use function signatures, you get a more accurate and descriptive API like this:
Function overloading helps you build a safer API design, especially in large projects. However, compared to OOP languages, it relies on manual checks and may clutter your code. It’s not a pattern you’ll encounter frequently.
See you in other articles 👋