DEV Community

Chris Cook
Chris Cook

Posted on • Edited on • Originally published at zirkelc.dev

Function Overloading: How to Handle Multiple Function Signatures

Function overloading in TypeScript allows you to have multiple functions with the same name but with a different signature (parameters and types). This means that the parameters have different types or the number of parameters is different for each function. The correct function to call is determined at runtime based on the arguments passed.

For example, consider a function that takes an argument and returns its square. If the argument is a number, you can calculate the square by multiplying the number by itself. However, if the argument is an array of numbers, you can calculate the square by squaring each element of the array.

function square(x: number): number;
function square(x: number[]): number[];
function square(x: number | number[]): number | number[] {
    if (typeof x === 'number') {
        return x * x;
    } else {
        return x.map(e => e * e);
    }
}

square(3); // => 9
square([3, 5, 7]) // => [9, 25, 49]
Enter fullscreen mode Exit fullscreen mode

Here the first two lines describe the function signature with two different argument types, number and number[]. The actual function implementation begins on the third line, and its signature must be a union of number | number[]. The implementation uses the typeof operator to determine which branch of the implementation to use.

Another example would be a database query function that reads a user from the database based on an ID (number), username (string), or attributes (object).

function query(id: number): User;
function query(username: string): User;
function query(attributes: Record<string, any>): User;
function query(arg: number | string | Record<string, any>): User {
    let condition = '';

    if (typeof arg === 'number') {
        // Query the database using an id
        condition = `id: ${arg}`;
    } else if (typeof arg === 'string') {
        // Query the database using a username
        condition = `username: "${arg}"`;
    } else {
        // Query the database using attributes
        condition = JSON.stringify(arg);
    }

    return db.query(`SELECT * FROM User WHERE ${condition}`);
}

query(1); // => id: 1
query('johndoe'); // => username: johndoe
query({ firstName: 'John', lastName: 'Doe' }); // => attributes: {"firstName":"John","lastName":"Doe"}
Enter fullscreen mode Exit fullscreen mode

In this example, the query function is overloaded three times, each time with a different signature. The first signature accepts a single parameter of type number, the second signature accepts a single parameter of type string, and the third signature accepts a single parameter of type Record<string, any>, which is a simple key-value map.

The examples I gave earlier show how you can use function overloading to process different types of input and give you an idea of how it can be useful in practice.

Top comments (9)

Collapse
 
brense profile image
Rense Bakker

You can also use the is keyword to determine which type of parameters was provided:

function isNumbersArray(args: number | number[]): args is number[] {
  return Array.isArray(args)
}

// usage
if(isNumbersArray(args)){
  // args is typed as number[] here...
} else {
  // args is typed as number here...
}
Enter fullscreen mode Exit fullscreen mode
Collapse
 
zirkelc profile image
Chris Cook

Yes, that's a good addition.

Collapse
 
blindfish3 profile image
Ben Calder

I'm looking at this and trying to figure out how this is useful in practice 😅
What's the benefit of defining the overloaded functions when the actual implementation has to include all possible types in each parameter slot anyway?

Collapse
 
maciekbaron profile image
Maciek Baron

The main benefit is that you get the correct return type based on the parameter used.

When using const a = square(2), a is a number, and when using const a = square([1,2,3,4]), a is a number[]. Without the overloading, in both cases a would be number | number[].

Collapse
 
zirkelc profile image
Chris Cook • Edited

IMHO, the advantage of overloading a function is that there is a clear and precise function interface (signature) on the consuming side, i.e. the code that calls the function. It is much easier to understand what the function does if, for example, VSCode displays the three different options with the correct parameter name and type:

Image description

Image description

Image description

In plain JavaScript, you would have one parameter thats named like idOrUserNameOrAttributes or more arbitrarily param and you have to figure what types are supported.

Regarding your second question: you don't actually need to include all possible types in the implementation signature. The implementation can simply use any instead of the union type:

// implementation signature can use a union type
function query(arg: number | string | Record<string, any>): User { }
// or it simply use any
function query(arg: any): User { }
Enter fullscreen mode Exit fullscreen mode

Another great thing is that you can have a different number of parameters for each overloaded signature. For example, a date formatting function would have four signatures with a different number of parameters:

function formatDate(timestamp: number): Date;
function formatDate(iso: string): Date;
function formatDate(month: number, day: number, year: number): Date;
function formatDate(month: number, day: number, year: number, hour: number, minutes: number, seconds: number): Date;
function makeDate(timestampOrIsoOrMonth: any, day?: number, year?: number, hour?: number, minutes?: number, s?: number): Date {
  // ...
}
Enter fullscreen mode Exit fullscreen mode

Again, an editor like VSCode will display the correct function signature depending on the parameters passed, e.g. if you pass three numbers, the function will display for month, day and year.

Collapse
 
blindfish3 profile image
Ben Calder

So ultimately there's the benefit of getting better argument hints; at the cost of extra noise/complexity in the code. Since there isn't support for true function overloading in JS I'd typically define each 'overloaded' function with a separate function name and, if need be, have a 'parent' function that determines which sub-function to call depending on argument types. It would be nice if there were a way to type the parent function based on the children 🤔

Collapse
 
dungphan profile image
dungphan

This didn't demonstrate a use case of parameter shifting.

Something likes server.listen(...)

listen(port?: number, hostname?: string, backlog?: number, listeningListener?: () => void): this;
listen(port?: number, hostname?: string, listeningListener?: () => void): this;
listen(port?: number, backlog?: number, listeningListener?: () => void): this;
listen(port?: number, listeningListener?: () => void): this;
listen(path: string, backlog?: number, listeningListener?: () => void): this;
listen(path: string, listeningListener?: () => void): this;
listen(options: ListenOptions, listeningListener?: () => void): this;
listen(handle: any, backlog?: number, listeningListener?: () => void): this;
listen(handle: any, listeningListener?: () => void): this;
Enter fullscreen mode Exit fullscreen mode
Collapse
 
tylim88 profile image
Acid Coder

avoid function overloading if possible, because of the confusing type hint

most of the time union type is sufficient, else use conditional type with JSDoc and type level literal error message

Collapse
 
zirkelc profile image
Chris Cook

Which type hint confuses you?