One thing to keep in mind is that sometimes you may outsmart the compiler. TypeScript utilizes certain methods to analyze code control flow in order to deduce narrower types for expressions. While it does a decent job at this, it's not foolproof and may never be.
For example, when you access obj.bar
directly after throwing an error if isA(obj)
returns false
, the compiler narrows down obj
to include A
as expected. However, things change when you create a closure and pass it to Array.prototype.forEach()
. The compiler reverts obj
back to its original type that does not include
A</code. Even though we know that <code>forEach()
will immediately call its callback function, TypeScript doesn't have that foresight. It fears that the value of
obj
could be altered before the callback invocation, leaving it with no option but to give up on narrowing.
A workaround would be to declare obj
as a const
instead of using let
:
const obj = { foo: 'hello' };
if (!isA(obj)) throw 'will never throw'
Array(5).fill(0).forEach((_, i) => {
obj.bar // This works fine now
});
Although this doesn't guarantee immutability of obj.bar
, TypeScript leans towards thinking "const
is less likely to change than let
", despite evidence suggesting otherwise.
Another way around this issue, if making obj
a const
isn't possible, is to assign a new const
variable post-narrowing, which can then be used in the callback:
let obj = { foo: 'hello' };
if (!isA(obj)) throw 'will never throw'
const myObj = obj;
Array(5).fill(0).forEach((_, i) => {
myObj.bar // This is acceptable
});
Alternatively, you could just skip using obj
altogether:
let obj = { foo: 'hello' };
if (!isA(obj)) throw 'will never throw'
const bar = obj.bar;
Array(5).fill(0).forEach((_, i) => {
bar // This also works
});
The choice is yours. Hopefully, this sheds some light on the situation. Best of luck!