Narrowing only occurs on union types.
In the first scenario, when you assign directly to the property x
, which has a union type of number | string | undefined
, the control flow narrows it down to number
within the scope. However, in the second scenario, when you assign to an object that is not a union, the control flow does not narrow it.
The narrowing is limited to the current scope, and will not persist in a nested function, for example.
I understand your point, it may seem like TypeScript could handle this situation, but unfortunately, it currently does not. The control flow analysis is constantly being improved.
If you change your type to a union, then assigning a literal object will cause narrowing to occur:
type MyObj = { x?: number | string }
let obj1: MyObj = {}
obj1.x = 1 // assign x with dot
obj1.x // number
function anotherScope() {
obj1.x // number | string | undefind
}
let obj2: MyObj;
obj2 = { x: 1 }; // assign x with object literal
obj2.x // number | string | undefind
obj2.x = 2;
obj2.x // number
let obj3: { x: number } | { x: string } | { x?: undefined };
obj3 = { x: 1 } ; // assign x with object literal
obj3.x // number, narrowed to first member of the union
let obj4: { x?: number } | { x?: string }
obj4 = { x: 1 } ; // assign x with object literal
obj4.x // number | undefind, narrowed to first member of the union
Playground
These are the relevant issues discussing the design limitation in TypeScript:
1.
2.