Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Typescript - Incorrectly inferring 'never'

This is a basic use-case: Initializing a variable with null then changing the value in some nested loop/function:

let a: number | null = null;
[1].forEach(() => {
  a = 1;
});

if (a != null)
  a.toFixed(); // Error: Property 'toFixed' does not exist on type 'never'.

However typescript infers a's type to be never. I would assume that without the if it would assume it to be null | number in which case I could get an error stating the property does not exist on null, but why is it assuming it to be never based on just the initial assignment value.

Am I doing something wrong?

like image 617
user3690467 Avatar asked Feb 19 '19 15:02

user3690467


2 Answers

If you are absolutely sure that a has a value there, than you can put the ! after the variable

let a: number | null = null;
[1].forEach(() => {
  a = 1;
});

if (a !== null)
  a!.toFixed(); //

I would not use null thought but undefined, so no need to use !

let a: number | undefined;
[1].forEach(() => {
  a = 1;
});

if (a) { // <-- if undefined or 0
  a.toFixed(); // No problem here
}

Also as recommendation use !== not !=

like image 107
distante Avatar answered Nov 10 '22 12:11

distante


Late to the party, but here are my 2 cents.

Remark to the accepted answer

if (a) {
  a.toFixed(); // No problem here
}

note, that the if block will not be invoked when a is 0.

  • to fix this, use if (a !== undefined)
  • otherwise (when you really don't want to handle 0 you are better off initializing a to 0 like so:
    let a = 0; // typescript will infer the type number
    ...
    if (a) {
      // a is of type number and !== 0
    }

Answer to a comment

Why would you initialize a variable using undefined?

People sometimes do this, because some tools (IDE, linters, ..) report errors/warnings otherwise.

e.g. here is a warning when you use IntelliJ IDEA with the default typescript settings:
enter image description here

I recommend to deactivate these checks, since an uninitialized variable in javascript always has the value undefined: i.e. in some other languages (i.e. C), the variable might have some random "garbage" value.

Quote from MDN: Global_Objects/undefined#description

A variable that has not been assigned a value is of type undefined.

And for all other values (i.e. values that are not undefined), the typescript compiler will show an error:
TS2454: Variable 'xxx' is used before being assigned.

Answer to the original question

let a: number | null = null;
[1].forEach(() => {
  a = 1;
});

if (a != null)
  a.toFixed(); // Error: Property 'toFixed' does not exist on type 'never'.

This only happens when the compiler option strictNullChecks is on.

This quote describes the reason well (Quote Reference)

While strictNullChecks implies that it is just checking for usage of variables that might be undefined or null it really turns the compiler into a very pessimistic mode, where when there are no contextual way of inferring the type, it will choose the narrowest type, instead of the widest type,

This means, in detail:

  • since the typescript compiler is not smart enough to know if the forEach loop is invoked (and thus a value is assigned), it takes the pessimistic approach and assumes that x remains null
  • thus, the type of x after the loop is null (not number | null as we might expect)
  • now, the final if block checks if x !=== null which can never be the case (since typescript assumes that x is null when the if-statement is executed. Thus the type of x inside of the if-statement is never
  • so one "solution" is to explicitly tell typescript that you are sure, that the value of x is defined by using x!.toFixed()

Misc

strictNullChecks

When strictNullChecks is off, the code works: TypeScript example: strictNullChecks=off
I strongly recommend NOT to do that.

for..of loop

When you use a for..of loop instead of forEach() the code works, even when strictNullChecks is on: Playground

let a: number | null = null;
for (const i of [1]) {
  a = 1;
};
if (a != null)
  a.toFixed();

Other init values

You can also consider other initialization values (instead of undefined or null): Playground

let a = 0; // typescript will infer that a is of type number
[1].forEach(() => {
  a = 1;
});
if (a >= 0)
  a.toFixed();


let b = NaN; // typescript will infer that b is of type number
[1].forEach(() => {
  a = 1;
});
if (!isNaN(b))
  b.toFixed();
like image 2
TmTron Avatar answered Nov 10 '22 10:11

TmTron