Mastering Function Overloading in TypeScript for Flexibility
Written on
Have you ever needed to create a function in TypeScript that accommodates different types of input parameters? Function overloading is a valuable capability that enables you to achieve this. It allows multiple functions to share the same name but differ in their parameter types and/or return types, which is particularly handy for writing functions that can process various kinds of input.
To illustrate how function overloading operates in TypeScript, consider a scenario where we want to construct a function that accepts two numbers and returns their sum. This can be implemented like so:
function add(a: number, b: number): number {
return a + b;}
This function performs well for adding two numbers. However, if we want to add two strings instead, we can adjust the function as follows:
function add(a: number | string, b: number | string): number | string {
if (typeof a === "number" && typeof b === "number") {
return a + b;} else if (typeof a === "string" && typeof b === "string") {
return a + b;} else {
throw new Error("Invalid arguments");}
}
console.log(add(3, 5)); // 8
console.log(add("3", "5")); // 35
console.log(add("3", 5)); // Invalid arguments
In this example, we utilize a union type to permit the add function to accept both numbers and strings as inputs. We also employ an if/else structure to verify the types of the parameters, returning either a number or a string based on the inputs.
This example, while straightforward, highlights how function overloading can be extremely useful for crafting functions that manage diverse input types.
To define an overloaded function in TypeScript, you declare the function signature without an implementation, followed by the specific implementations. Let’s redefine our earlier function using this method:
function add(a: number, b: number): number; // signature 1
function add(a: string, b: string): string; // signature 2
function add(a: any, b: any): any {
return a + b;}
console.log(add(3, 5)); // 8
console.log(add("3", "5")); // 35
console.log(add("3", 5)); // No overload matches this call.
Here, we have two function signatures for add: one for two number inputs and another for two string inputs. The implementation is provided in a third function declaration that accepts any two arguments and returns their sum or concatenation according to their types.
Let’s examine another example: suppose we want a function that returns the length of a string:
function getLength(str: string): number {
return str.length;}
console.log(getLength("If you have any questions")); // 25
This function works perfectly for fetching the length of a string. But what if we also want to measure the length of an array? Here’s how we can use function overloading to cater to both needs:
// Function signatures
function getLength(arr: any[]): number;
function getLength(str: string): number;
// Function implementation
function getLength(arg: any): number {
if (Array.isArray(arg)) {
return arg.length;} else if (typeof arg === "string") {
return arg.length;} else {
throw new Error("Invalid argument");}
}
console.log(getLength("If you have any questions")); // 25
console.log(getLength(["tuple", "array"])); // 2
console.log(getLength(["If", "you", "have", "any", "questions"])); // 5
console.log(getLength(3)); // Invalid argument
In this instance, we defined two signatures for getLength: one for an array and one for a string. The implementation checks the type of the input and computes the length accordingly.
For a more advanced example, let's create a function that generates a rectangle object based on provided coordinates and dimensions:
type Coordinates = { x: number; y: number };
type Size = { width: number; height: number };
// Function signatures
function createRectangle(options: Coordinates & Size): { x1: number; y1: number; x2: number; y2: number };
function createRectangle(x: number, y: number, width: number, height: number): { x1: number; y1: number; x2: number; y2: number };
// Function implementation
function createRectangle(arg1: number | Coordinates, arg2?: number, arg3?: number, arg4?: number): { x1: number; y1: number; x2: number; y2: number } {
if (typeof arg1 === "object") {
const { x, y, width, height } = arg1 as Coordinates & Size;
return { x1: x, y1: y, x2: x + width, y2: y + height };
} else if (typeof arg1 === "number") {
return { x1: arg1, y1: arg2!, x2: arg1 + arg3!, y2: arg2! + arg4! };}
throw new Error("Invalid arguments!");
}
const rectangle1 = createRectangle(0, 0, 100, 50);
console.log(rectangle1); // { x1: 0, y1: 0, x2: 100, y2: 50 }
const rectangle2 = createRectangle({ x: 10, y: 10, width: 20, height: 30 });
console.log(rectangle2); // { x1: 10, y1: 10, x2: 30, y2: 40 }
We designed a function named createRectangle that can produce a rectangle object based on either an object with specific properties or four individual arguments. The return object contains the properties x1, y1, x2, and y2, defining the rectangle's coordinates.
This function can be invoked in two ways: by passing an object with x, y, width, and height attributes or by using four separate numerical arguments. The implementation checks the type of the first argument to decide which signature to apply.
Lastly, we can call the createRectangle function with both types of arguments and log the resulting objects to the console.
If you have any questions, feel free to reach out in the comments.
If you enjoy this kind of content and want to support me, consider buying me a coffee or clicking the clap button below a few times to show your appreciation. Your support is crucial for me to continue writing — thank you.
Want to Connect?
LinkedIn - Twitter - GitHub
TypeScript Reading List: Reference & Tips And Tricks Edit description chntrks.medium.com