'Overload Signatures and the Implementation Signature in typescript

I am reading Typescript handbook and I am having a hard time to understand why the following code snippet have error message:

function fn(x: string): void;
function fn(vo) {
  // ...
}
// Expected to be able to call with zero arguments
fn();

Here is the explanation but I could not understand it, Could anyone explain to me what is going on here?

The signature of the implementation is not visible from the outside. When writing an overloaded function, you should always have two or more signatures above the implementation of the function.



Solution 1:[1]

I was extremely confused by this also. Coming from a Kotlin/Java Android background, I have to say the overloading machines of Typescript take a while to grasp.

First, we need to address how to declare overload in Typescript

Concern 1: Incorrect way of declaring overloads [not the cause of issue for the question]

My initial idea about handling overloading is to write the following code:

function makeDate(timestamp: number): void { /* code */ }
function makeDate(m: number, d: number, y: number): void { /* code */ }

I was immediately hit by Duplicate function implementation.ts(2393) lint error. I was confused at first, since this is how you declare overload in Java and Kotlin. However, this is actually not how you declare overload in Typescript and I believe this is the main cause of issues.

Answer to concern 1:

Turns out in Typescript, the declaration of overload is not by having two methods with the same name and different signature with each of their own implementations. Instead, it is by defining methods with the same name and different signatures without body first, and lastly provide a method with implementation (method body), that has the ability to handle all the previously declared bodyless methods.

Concern 2: Incorrect amount of argument for the overload requirement [cause of issue for the question]

Now that I have figured out the correct way to create overloaded methods in Typescript, I went straight ahead to write the following code:

[code 2.1]

function fn(x: string): void; // <-- Define one way to call method
function fn(x: string, y: string, z: string): void; // <-- Define another way to call method with more param (overloaded way)
function fn(x: string, y?: string, z?: string): void { // <--- provide a method with a body that can handle both previous two declarations
    if (y === undefined) {
        console.log("branch 1")
    } else {
        console.log(`branch 2 -> ${x} :: ${y} :: ${z}`)
    }
}

fn("x")
fn("xxx", "yyy", "zzz")

Outputs:

$ branch 1
$ branch 2 -> xxx :: yyy :: zzz

In the code snippet above, we declared 1 method with 2 different overloads.

  1. overload 1 -> function fn(x: string): void;
  2. overload 2 -> function fn(x: string, y: string, z: string): void; And they are both handled by "concrete" method:
function fn(x: string, y?: string, z?: string): void {
    if (y === undefined) {
        console.log("branch 1")
    } else {
        console.log(`branch 2 -> ${x} :: ${y} :: ${z}`)
    }
}

Since this method has a body, that is why I call it concrete method. Also notice that this method handles the case with @params: x, and with @params: y and z being optional, by doing this it covers the method calls {overload1 and overload2}. And this is a valid overload.

Following the same mindset, I then went ahead and wrote the following code:

[code 2.2]

function fn2(x: string): void;
function fn2(x: string, y: string): void;
function fn2(x: string, y?: string): void {
    if (y === undefined) {
        console.log("branch 1")
    } else {
        console.log(`branch 2 -> ${x} :: ${y}`)
    }
}

fn2("x")
fn2("x", "y")

Outputs:

$ branch 1
$ branch 2 -> x :: y

The code also worked, which just like what I suspected. So I wrote some more code:

[code 2.3]

function fn3(): void;
function fn3(x?: string): void {
    if (x === undefined) {
        console.log("branch 1")
    } else {
        console.log(`branch 2 -> ${x}`)
    }
}

fn3()
fn3("x")

But this time the code did not work and tsc is complaining:

Playground.ts:219:5 - error TS2554: Expected 0 arguments, but got 1.

219 fn3("x")
        ~~~

Found 1 error.

Now, this is the time we should spend some time understanding the quote from the documentation:

The signature of the implementation is not visible from the outside. When writing an overloaded function, you should always have two or more signatures above the implementation of the function.

This was very confusing if you overthink it, turns out you can simply understand it as "You must have 2+ args in order to do overloads", that is why the code sample [code 2.1] and [code 2.2] worked. Since:

  • For the [code 2.1] it had 1 and 3 args. Which fits the requirement mentioned documentation
  • For [code 2.2] it had 1 and 2. Which also fits the requirement in the docs.
  • However, for [code 2.3] it had 0 and 1. This does not fit the requirement of you should always have two or more signatures above the implementation of the function in the docs, that is why the tsc is complaining.

And this actually makes sense, since:

function fn3(): void;
function fn3(x?: string): void {
    if (x === undefined) {
        console.log("branch 1")
    } else {
        console.log(`branch 2 -> ${x}`)
    }
}

Is the same as just having defined one argument with ? optional param:

function fn3(x?: string): void {
    if (x === undefined) {
        console.log("branch 1")
    } else {
        console.log(`branch 2 -> ${x}`)
    }
}

So, Answer to concern 2:

Overload method declaration only works for method with 2 and 2+ arguments, and a method with a body is required to handle all the cases you declared for the overloads. Also, this does not work with methods that have 0 to 1 arguments, tsc will complain Expected 0 arguments, but got 1.ts(2554) in that case. In the meantime, declare overload for methods with 0 and 1 arguments is redundant, since you can just declare a method with 1 optional param.

Solution 2:[2]

INTRO - READ

In the following answer, I try to give my point on view on why Haomin's answer is not correct and why it doesn't show the implementation signature to the outside. You can jump to the section you want, it has the title on it.

HAOMIN'S ANSWER

To me, it really doesn't make sense @Haomin's answer. For example:

Overload method declaration only works for method with 2 and 2+ arguments...

What the docs say is that:

The signature of the implementation is not visible from the outside. When writing an overloaded function, you should always have two or more signatures above the implementation of the function.

Signatures ARE NOT arguments. The arguments are the values passed to the function when it is called. The signature of the function is different (the name, params, type, etc. of the function) -> Signature of a function - MDN Docs.

You will see his example working if you do it the following way:

function fn3(): void;
function fn3(x: string): void;
function fn3(x?: string): void {
    if (x === undefined) {
        console.log("branch 1")
    } else {
       console.log(`branch 2 -> ${x}`)
    }
}

fn3()
fn3("x")

All I did was add another signature that accepted the parameter (x). Why? Because as the docs say, the implementation signature is not seen from the outside. The implementation signature is the last signature, in this case:

function fn3(x?: string): void)

So to wrap it all: Make sure ALL the needed signatures are written above the implementation signature, keep in mind that the implementation signature is not seen from the outside and that it must contain all of the other signatures possibilities.

WHY IS THE IMPLEMENTATION SIGNATURE NOT SHOWN TO THE OUTSIDE

(thought that came to me, there might be other reasons too). Take the following code as an example:

function fn3(x: string): void;
function fn3(x: string, y: number, z: number): void;
function fn3(x: string, y?: number, z?: number): void {
    if (y === undefined) {
        console.log(`X is -> ${x}`)
    } else {
        console.log(`X is -> ${x}`)
        console.log(`Y is -> ${y}`)
        console.log(`Z is -> ${z}`)
    }
}

fn3("x")
fn3("x", 2, 3)

Imagine that it accepted the implementation signature, what would have happened if we had called fn3 as fn3("x", 2) without the third param since it is optional? It would have crashed or given undefined. That is very possibly why TS doesn't 'show' the implementation signature to the outside, because things like this would have happened. David

Sources

This article follows the attribution requirements of Stack Overflow and is licensed under CC BY-SA 3.0.

Source: Stack Overflow

Solution Source
Solution 1
Solution 2 DharmanBot