This limitation in TypeScript's capability to conduct control flow analysis is widely recognized. Typically, the compiler can narrow down the presumed type of a union-typed variable upon assignment. However, it fails to track the outcomes of control flow analysis across function boundaries. The compiler operates under the assumption that called functions do not impact the apparent type of any variables. For an authoritative explanation of this issue, refer to microsoft/TypeScript#9998.
When initializing
let x:number | undefined = undefined
, the compiler narrows down the type of
x
from
number | undefined
to just
undefined
. By nature, the compiler cannot predict if the callback function
res => { x = 1; res(x) }
will ever be executed. Therefore, it chooses not to assume that
x
holds the value
1
. To maintain type safety, the compiler would need to consider the possibility of the function call and revert the type of
x
back to
number | undefined
. Yet, doing so would render control flow analysis virtually worthless. Hence, TypeScript adopts an "optimistic" heuristic approach by assuming that function calls are devoid of side effects, even though this assumption may be incorrect in certain cases.
In your scenario, apart from potential runtime modifications, my recommendation would either involve utilizing a type assertion in the declaration of x
, allowing you to prevent the initial 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
}
}
Alternatively, you could employ a type assertion later to inform the compiler that the value is truly 1
despite its lack of realization:
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
}
Unfortunately, there isn't a straightforward method to widen x
itself without affecting runtime (x = x as number | undefined as 1
could work but has runtime implications).
Access the code on Playground