I think what's happening here is that the rules for narrowing upon assignment only apply to values of union types, for better or worse:
An assignment (including an initializer in a declaration) of a value of type S to a variable of type T changes the type of that variable to T narrowed by S in the code path that follows the assignment.
[snip]
The type T narrowed by S is computed as follows:
- If
T is not a union type, the result is T.
- If
T is a union type, the result is the union of each constituent type in T to which S is assignable.
It has been suggested to lift this restriction, but for now that's the way it is.
Also note that the type A | B is a union, while the type {x: A | B} is not. Yes, {x: A | B} has a property whose type is a union type, but it is not itself a union type.
So let's look at what happens here:
let myObj1: { myProp: string | number } = { myProp: 5 };
Okay, that's assigning a value of a non-union type. So we can expect no narrowing here. Let's make sure:
myObj1.myProp.toFixed(); // error!
Yep, no narrowing happened. Moving on:
myObj1.myProp = "5";
Okay, that's assigning a value of type string | number, a union type. So we can expect narrowing here. Let's make sure:
console.log(myObj1.myProp.length); // no error
Yep, narrowing. And then the rest of it:
let myObj2: { myProp: string | number } = { myProp: 5 };
myObj2 = { myProp: "5" };
Those are both assigning values of non-union types... so, no narrowing:
myObj2.myProp.length; // error!
Yep, no narrowing.
So that's probably the canonical explanation for why this happens. As for whether narrowing should happen for all assignments... if you feel it should, you might want to go to the relevant Github issue, microsoft/TypeScript#27706 and give it a or describe your use case if you think it's particularly compelling.
Okay, hope that helps; good luck!
Link to code