Typescript function overloads strange behavior

Viewed 135

I'm trying to learn how function overloads work in Typescript, but I don't get it. Check this snippet (ts playground) (using only one declaration to keep it as simple as possible):

function foo(x: number): 42

function foo(x: number) {
   return x
}

const y = foo(23)

Two things are amiss:

  • no TS error on function declaration, even when the actual return type clearly doesn't match the overload signature
  • the inferred value of y is 42

screenshot from ts playground

If instead I just declare the return type on the function implemenation, things work (or fail!) as expected:

function foo(x: number): 42 {
   return x // -> Type 'number' is not assignable to type '42'.(2322)
}

const y = foo(4)
1 Answers

Let's tackle the first issue:

no TS error on function declaration, even when the actual return type clearly doesn't match the overload signature

If you take a look at this doc (since then deprecated but I guess it still works the same), they say:

In order for the compiler to pick the correct type check, it follows a similar process to the underlying JavaScript. It looks at the overload list and, proceeding with the first overload, attempts to call the function with the provided parameters. If it finds a match, it picks this overload as the correct overload. For this reason, it’s customary to order overloads from most specific to least specific.

My guess is that it the same when you write the implementation, for each overload that you declare, it will try to call your function with it. I don't know if that makes sense. Let's see an example:

function foo(x: number): 42 // ok since 42 is compatible with number (number is wider than 42)

function foo(x: number) {
   return x
}

const y = foo(23)

If you take the signature, and try to call your function with it, it's 'compatible'. x is a number and 42 is also a number and the return type of our implementation says that x is a number.

If I do this:

function foo(x: number): 42 // error: not compatible since 42 is not compatible with 32

function foo(x: number) {
   return 32
}

I get an error. Another one for the road:

function foo(x: number): 'text1'
function foo(x: string): 'text2'

function foo(x: number | string) {
  return x // why is there no error?
}

//y is "text1" (no, it's a number!!!)
const y = foo(42)


//there is no error because if you take the signature, you can call your function with it -> x is a number so compatible with number | string and 'text1' is compatible with number | string

So why is it like this might you ask? If you look at it in the following way, it will be easier to reason about.

Not

Can I call my signature with my function implementation?

instead the other way around

If I take one of my signatures, can I call my function implementation with it?

Don't see the overloads as some sort of requirements that the function implementation has to meet. Have you noticed that the errors are on the overloads and not on the function implementation? It's the overload that have to be compatible with the function implementation.


The second point:

the inferred value of y is 42

If you hover over your function implementation, you'll see that it just displays the overloads.

The latest version of the doc says:

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.

They provide a common mistake that people make:

function fn(x: string): void;
function fn() {
  // ...
}
// Expected to be able to call with zero arguments
fn();
Expected 1 arguments, but got 0.

The implementation itself doesn't count as an overload.

So only the overload is seen when calling the function:

const y = foo(23)

So with this in mind, use them overloads wisely. The rest is your responsibility. By the way, in your use case you don't need to use overloads at all. You should use it only when you have at least two of them.

Related