Why cant typescript infer the return type when using Differentiating Type checks

Viewed 309

I want to understand why TypeScript can't infer the return type of the following function (whilst it is able to differentiate in the if-else statement):

function calc (arg: number|string) {
  if (typeof arg === 'number') {
    // here typescript knows arg is number type
    return arg 
  } else if (typeof arg === 'string') {
    // here typescript knows arg is string type
    return arg 
  }
}

// infers test to be  number|string|undefined
const test = calc(10)

And when we write the following function:

function calc (arg: number|string) {
  return String(arg)
}

// infers test to be string
const test = calc(10)

Then it of course can infer the return type. But how come it cannot infer the return type of the first function while it does provide type safety in the different branches and it knows it gets a number type as arg?

Edit

I know how to solve it, but I really want to understand why it TypeScript can't do it?

5 Answers
function calc (arg: number): number;
function calc (arg: string): string;
function calc (arg: number|string) {
  if (typeof arg === 'number') {
    // here typescript knows arg is number type
    return route
  } else if (typeof arg === 'string') {
    // here typescript knows arg is string type
    return route
  }
}

You can specify what the return value is for a particular argument type by overloading the original function definition.

See also: https://www.typescriptlang.org/docs/handbook/functions.html#overloads

Typescript doesn't do this automatically because that would require evaluating your function, i.e. actually running the code. Even though the parameters are known at compile time and it might theoretically be possible it doesn't try anything like that. Maybe in this case it seems like it should be simple but that's not true in the general case.

When it narrows the type for you inside the block, it is saying only that if the conditional is true, then the type is X, but it's not actually evaluating the conditional with a value.

if the return type is supposed to be the same as the input you can also use Generic

function calc <T extends number|string>(arg: T): T {
  if (typeof arg === 'number') {
    // here typescript knows arg is number type
    return arg
  } else if (typeof arg === 'string') {
    // here typescript knows arg is string type
    return arg
  }
  throw new Error('should not reach here')
}

const t = calc(12)

The function you have is in essence just an identity function, it will return the argument. So if the argument is of type X the return type must also be of type X (at least).

You did not specify all branches ( else block) so theoretically, it could also return undefined.

Therefore it infers that the overall type should be: X | undefined -> string | number | undefined

What else could it infer?

In the first example you forced the function to return String(arg), so no matter what the arguments be, it'd always return a string.

In the code

function calc (arg: number|string) {
  if (typeof arg === 'number') {
    // here typescript knows arg is number type
    return arg 
  } else if (typeof arg === 'string') {
    // here typescript knows arg is string type
    return arg 
  }
}

There are three paths through the code:

  1. number
  2. string
  3. something other than a number or a string

In the third case, there is no return type, thus the signature of the return is number | string | undefined

You can see this if you examine the code in the TypeScript playground

function calc(arg: number | string): string | number | undefined
Not all code paths return a value.(7030)

If you want your variable to "be" the single type based on the passed in argument, you can do something like this:

function calc3<T>(arg: T) {
    return arg 
}

let isAnInt = calc3(3);
let isAString = calc3('hello');

Where isAnInt is typed as a number and isAString is typed as a string, and this is based on the type passed into the function.

Related