Skip to content
Theory · Functions · lesson 9 of 10

Function overloads

Some machines have two plates. One says: "Coins — gives tea". The other: "Card — gives tea and a receipt". The mechanism inside is one, but what you get depends on how you pay, and each plate tells you that honestly for its own case.

A function sometimes needs to say "whatever kind you give me, I return the same kind". Take double: it doubles a string by joining it to itself ("ab" → "abab") and a number by multiplying it (21 → 42). With one signature built on a union, you get this:

TypeScript
function double(x: string | number): string | number {
    if (typeof x === "string") {
        return x + x;
    }
    return x * 2;
}

console.log(double(21).toFixed(1)); // ✗
TEXT
error TS2339: Property 'toFixed' does not exist on type 'string | number'.

The signature promises: "a string or a number comes in, a string or a number goes out". It has no link between the input and the output. So double(21) has the type string | number, and on such a value you may call only what both types have. At run time double(21) would return 42, but tsc cannot know that from the signature.

Overload signatures

Function overloads are several signatures without a body that stand before one function with a body. Each overload is a plate of its own: which argument it takes and what it returns for it.

TypeScript
function double(x: string): string;
function double(x: number): number;
function double(x: string | number): string | number {
    if (typeof x === "string") {
        return x + x;
    }
    return x * 2;
}

console.log(double("ab"), double(21).toFixed(1)); // abab 42.0

Let's go line by line:

  1. function double(x: string): string; is the first overload: a string in, a string out. It has no body and ends with a semicolon.
  2. function double(x: number): number; is the second overload, for numbers.
  3. The third declaration is the implementation, the only function with a body. Its parameter accepts everything the overloads allow, and the body itself checks with typeof what came in.
  4. Now double(21) has the type number, the .toFixed(1) call passes, and the program prints abab 42.0.

For every call tsc tries the overloads from top to bottom and takes the first one the arguments fit. The call gets that overload's result type.

What the calling code sees

From outside only the overloads are visible, and the implementation's signature is hidden. In tsc's messages this function's type looks like this: { (x: string): string; (x: number): number; }. Both plates are there, and the string | number from the implementation is not.

Hence the rule: a call must fit at least one overload as a whole. It does not help that the implementation would accept the argument: tsc does not use its signature for calls. If no overload fits, tsc reports error TS2769.

When a union is enough

Interviewers often ask: "Overloads or a union parameter?" Overloads are needed when the result type depends on the argument type, as with double. If the result is the same either way, one signature is enough:

TypeScript
function size(x: string | number[]): number {
    return x.length;
}

console.log(size("abc"), size([1, 2])); // 3 2

Both a string and an array have length, and in both cases the result is a number. The overloads size(x: string): number and size(x: number[]): number would add nothing. On the contrary, they would get in the way: a value of type string | number[] would fit neither of them.

Common mistake

A function with overloads was called with a value of a union type:

TypeScript
function double(x: string): string;
function double(x: number): number;
function double(x: string | number): string | number {
    if (typeof x === "string") {
        return x + x;
    }
    return x * 2;
}

function twice(v: string | number) {
    return double(v); // ✗
}
TEXT
error TS2769: No overload matches this call.

The message means: "no overload fits this call". v may be a string, so the second overload does not fit, and it may be a number, so the first one does not fit. This is where the machine analogy ends. A buyer always holds one thing in hand, but for tsc the value v is "either a coin or a card", and no plate promises anything for that. There are two fixes: check v with typeof before the call, or add a third overload function double(x: string | number): string | number;.

Key points

  • Overloads are several signatures without a body before one implementation; each links an argument type to a result type.
  • The calling code sees only the overloads: the implementation's signature is not visible from outside.
  • A call that fits no overload as a whole is error TS2769, including a call with a value of type string | number.
  • If the result type does not depend on the argument type, write one union parameter instead of overloads.