Home > database >  Typescript does not see a value set in an await
Typescript does not see a value set in an await

Time:10-06

Given the following code. The variable x is either undefined or a number and initially undefined. Inside the promise that is await'd x is then set to 1.

After the await'd promise, x is now 1 but typescript still thinks that it is undefined.

Am I doing something wrong here?

async function test() {
  let x:number | undefined = undefined
  await new Promise(res => {
    x = 1
    res(x)
  })
  if (x) {
    console.log(x)
  }
}

Playground

CodePudding user response:

This is a well-known limitation in TypeScript's ability to perform control flow analysis, which is what normally allows the compiler to narrow the apparent type of a union-typed variable upon assignment. The compiler does cannot track the results of control flow analysis across function boundaries, and it assumes that called functions have no effect on the apparent type of any variables. See microsoft/TypeScript#9998 for an authoritative description of the issue.

When you write let x:number | undefined = undefined, the compiler narrows the apparent type of x from number | undefined to undefined. Generally speaking the compiler cannot know whether or not the callback res => { x = 1; res(x) } ever gets executed, so it is probably correct for the compiler not to realize that x is 1. However, in order to be type safe, the compiler would have to assume that it might be called, and thus revert the type of x back to number | undefined. But it does not do this, because to do so would make control flow analysis nearly useless. TypeScript takes the "optimistic" heuristic solution of assuming function calls have no side effects, which in this case is unfortunately wrong.


In your case my suggestion (aside from any runtime refactoring) would either be to use a type assertion on your declaration of x to prevent the initial control flow narrowing to undefined:

async function test() {
  let x = undefined as number | undefined;
  await new Promise(res => {
    x = 1
    res(x)
  })
  // x is number | undefined here
  if (x) {
    x.toFixed(2); // no error now
  }
}

Or you could use a type assertion later to tell the compiler that the value is 1 even though it doesn't realize it:

async function test2() {
  let x: number | undefined = undefined;
  await new Promise(res => {
    x = 1
    res(x)
  });
  (x as number | undefined as 1).toFixed(2); // okay
}

I don't know of a great way to re-widen x itself without a runtime change (x = x as number | undefined as 1 would work but it has a runtime effect).

Playground link to code

CodePudding user response:

The code that sets x to 1 is in a separate function, and while you and i know that the promise constructor will call that function synchronously, typescript can't make that assumption. There are many cases in javascript where callback functions are called asynchronously, with the simplest being setTimeout. The type information says nothing about when the function will be called, so typescript has to assume the code will run asynchronously, which means it doesn't affect x's type after the await.

Since you're resolving the promise to 1 anyway, i'd recommend you change your code to this:

let x:number | undefined = undefined
x = await new Promise<number>(res => {
  res(1)
})

Now typescript knows we're dealing with a promise that resolves to a number, and since you await the promise and then assign its value to x, typescript knows a number will be assigned to x.

CodePudding user response:

Thought I would add my suggestion under @jcalz's answer as my own complementary answer. See @jcalz's answer to know the reason as to why this happens.

You can also remove the explicit assignment to undefined. That way, there's no narrowing at play and by the time we get to the if check, typescript will check whether the variable has been defined or not.

async function test() {
  let x: number | undefined
  await new Promise(res => {
    x = 1
    res(x)
  })
  // x is number | undefined
  if (x) { // is defined
    x.toFixed(2);
  }
}
  • Related