visit
I’ve used generics and union types in TypeScript before but I’ve never heard about function overloading. So this article is about taking a closer look at function overloading and comparing it with generics and union types.
Function overloading – some of JavaScript functions can be called in a variety of argument counts and types. In TypeScript, we can specify a function that can be called in different ways by writing overload signatures.
Let’s have a look at the example from the documentation:
function getLength(str: string): number;
function getLength(arr: number[]): number;
function getLength(x: any) {
return x.length;
}
Here we have a function to return a length
of a str
(one argument) or an arr
(one argument). To get overload signatures, we should write two function signatures: one accepting an argument with a string type, and another accepting argument with an array of numbers type.
Then, we have to write something called a compatible signature - a function implementation with a compatible signature. The previous two functions already have implementation signatures, but that signature can’t be called directly.
function getLength(x: number[] | string): number {
return x.length;
}
Always prefer parameters with union types instead of overloads when possible.
getLength(Math.random() > 0.5 ? "hello" : [0]);
No overload matches this call.
Overload 1 of 2, '(s: string): number', gave the following error.
Argument of type 'number[] | "hello"' is not assignable to parameter of type 'string'.
Type 'number[]' is not assignable to type 'string'.
Overload 2 of 2, '(arr: number[]): number', gave the following error.
Argument of type 'number[] | "hello"' is not assignable to parameter of type 'number[]'.
Type 'string' is not assignable to type 'number[]'.
In this case, because both overloads have the same argument count and same return type it is a good practice to use union types instead of function overloading to keep code clean and simple.
type FullOrderInfo = {
orderStatus: string;
deliveryDate: string;
orderNumber: string
}
type PartialOrderInfo = {
orderNumber: string;
}
function getOrderInfo(email: string): PartialOrderInfo;
function getOrderInfo(fullname: string, trackingNumber: string;, phoneNumber: string): FullOrderInfo;
function getOrderInfo(fullnameOrEmail: string, trackingNumber?: string, phoneNumber?: string): PartialOrderInfo & FullOrderInfo {
if (trackingNumber !== undefined && phoneNumber !== undefined) {
// there might be a request to backend which accepts strictly 3 args: fullname, trackingNumber and phoneNumber. Otherwise an error would be throught.
return {
orderStatus: 'inProgress';
deliveryDate: '20/10/2022';
orderNumber: '372956743728'
};
} else {
return {
orderNumber: '372956743728';
};
}
}
Here we have a function to return some orderInfo
that takes either an email
(one argument) or a fullname
, a trackingNumber
, a phoneNumber
(three arguments). So this function works only with 1 or 3 arguments (2 arguments are not acceptable). We’ve made two overload signatures and the final one is compatible signatures. Now let’s see what we get after calling this function with different counts of arguments:
const info1 = getOrderInfo('[email protected]'); // everithing is fine (one arg satisfies the condition)
const info2 = getOrderInfo('olgakiba', 'dh546kwL, '796677334'); // still fine (three args satisfy the condition)
const info3 = getOrderInfo('olgakiba', '796677334'); // No overload expects 2 arguments, but overloads do exist that expect either 1 or 3 arguments.
function getOrderInfo(fullnameOrEmail: string, trackingNumber?: string, phoneNumber?: string): PartialOrderInfo & FullOrderInfo {
// function body
}
Summing up it is a good practice to use union types or function overloading when you know all the argument types at the moment of the function declaration. Use union types when the return type doesn't change and function overloading when the return type does change depending on the argument's types.
type FullOrderInfo = {
orderStatus: string;
deliveryDate: string;
orderNumber: string
}
type PartialOrderInfo = {
orderNumber: string;
}
type GetOrderInfo = {
(email: string): PartialOrderInfo;
(fullname: string, trackingNumber: string, phoneNumber: string): FullOrderInfo;
};
const getOrderInfo:GetOrderInfo = function (fullnameOrEmail: string, trackingNumber?: string, phoneNumber?: string): PartialOrderInfo & FullOrderInfo {
if (trackingNumber !== undefined && phoneNumber !== undefined) {
return {
orderStatus: 'inProgress';
deliveryDate: '20/10/2022';
orderNumber: '372956743728'
};
} else {
return {
orderNumber: '372956743728';
};
}
};
type FullOrderInfo = {
orderStatus: string;
deliveryDate: string;
orderNumber: string
}
type PartialOrderInfo = {
orderNumber: string;
}
type GetOrderInfo = {
(email: string): PartialOrderInfo;
(fullname: string, trackingNumber: string, phoneNumber: string): FullOrderInfo;
};
const getOrderInfo:GetOrderInfo = (fullnameOrEmail: string, trackingNumber?: string, phoneNumber?: string): PartialOrderInfo & FullOrderInfo => {
if (trackingNumber !== undefined && phoneNumber !== undefined) {
return {
orderStatus: 'inProgress';
deliveryDate: '20/10/2022';
orderNumber: '733487232'
};
} else {
return {
orderNumber: '372956743728';
}
};
type FullOrderInfo = {
orderStatus: string;
deliveryDate: string;
orderNumber: string
}
type PartialOrderInfo = {
orderNumber: string;
}
type GetOrderInfo = {
(email: string): PartialOrderInfo;
(fullname: string, trackingNumber: string, phoneNumber: string): FullOrderInfo;
};
class OrderInfoGetter {
// Function declaration
getOrderInfo(email: string): PartialOrderInfo;
getOrderInfo(fullname: string, trackingNumber: string;, phoneNumber: string): FullOrderInfo;
getOrderInfo(fullnameOrEmail: string, trackingNumber?: string, phoneNumber?: string): PartialOrderInfo & FullOrderInfo {
// function body
};
// Function expression
getOrderInfo:GetOrderInfo = function (fullnameOrEmail: string, trackingNumber?: string, phoneNumber?: string): PartialOrderInfo & FullOrderInfo {
// function body
}
// Arrow function
getOrderInfo:GetOrderInfo = (fullnameOrEmail: string, trackingNumber?: string, phoneNumber?: string): PartialOrderInfo & FullOrderInfo => {
// function body
}
}
You might think: “Okay, now we know when to use union types and when — overloads, let’s go try it in practice”. But before you do this we have to define one problem between those solutions — having a possibility of adding type support when we are not aware of all the possible types beforehand. This is also a quite common case and as you can see it doesn’t satisfy all the conditions of using union types or function overloading.
function getFirstArrayElement<T>(array: T[]): T {
return array[0];
}
const firstArrayElement1 = getFirstArrayElement([1, 2, 3]); // return type: number
const firstArrayElement2 = getFirstArrayElement(["first", "second", "third"]); // return type: string
const firstArrayElement3 = getFirstArrayElement([true, false, true]); // return type: boolean
In other words, if you are writing a project from scratch and you already have the usage of function overloading in the code in most cases it means that your architecture might have some holes and you might need to reconsider it. Because first SOLID principle says:
Every class, module, or function in a program should have one responsibility/purpose in a program