I would like to know the difference between assigning an array item to variable and accessing directly to it. As I show in the code below, #1 hits the possibly null error, but #2 and #3 doesn't. Those results are same, right? Anyone knows how this is working?
interface NumInterface {
key1: number | null;
}
const numList: NumInterface[] = [{ key1: 1 }];
const fooFunc = (index: number) => {
// pass unchecked indexed accesses
if (!numList[index]) return;
// #1
// can not avoid "possibly null"
if (!numList[index].key1) return;
numList[index].key1 + 1; // this outputs "Object is possibly 'null'."
// #2
// can avoid "possibly null"
const target = numList[index].key1;
if (!target) return;
target + 1;
// #3
// can avoid "possibly null"
if (!numList[0].key1) return;
numList[0].key1 + 1;
};
The underlying issue here is a longstanding missing feature of TypeScript requested microsoft/TypeScript#10530.
TypeScript's control flow analysis, which lets the compiler see that x
must be truthy after if (!x) return;
, only works on property accesses when the property name is a known literal like 0
or "foo"
. If the property name is a wide type like number
or string
, or if it is a union type like 0 | 1
or "foo" | "bar"
, or if it is a generic type like I extends number
or K extends string
, the control flow analysis doesn't happen.
That's because currently the compiler just looks at the type of the indexer and not its identity. It can't see the difference between if (!obj[k]) return; obj[k].toFixed()
and if (!obj[k1]) return; obj[k2].toFixed()
if k
, k1
, and k2
are all of the same type. If that type happens to be a single literal type like 0
or "foo"
, then control flow analysis is fine, because then k1
and k2
would definitely hold the same value even though they are different variables. But if it's a wide type or a union type or a generic type, then control flow analysis is not safe because it could hold different values.
Again, this is a missing feature, not a problem with your code. Obviously there is a difference between if (!obj[k]) return; obj[k].toFixed()
and if (!obj[k1]) return; obj[k2].toFixed()
. The fact that the indexer k
is identical in both checks and is not reassigned between them guarantees that you are checking a property and then acting on the same property. But currently the compiler doesn't notice or act on this identity. The first time an attempt to fix microsoft/TypeScript#10530 caused an unacceptable degradation in compiler performance. It's possible that at some point it will be revisited with more performant code. If you want to go to that issue and give it a 👍 you can. It wouldn't hurt, but it probably won't help much either.
So that's why
if (!numList[index].key1) return;
numList[index].key1 + 1; // this outputs "Object is possibly 'null'."
doesn't work but
if (!numList[0].key1) return;
numList[0].key1 + 1;
does. And the reason
const target = numList[index].key1;
if (!target) return;
target + 1;
works is because target
is a const
variable and you are not checking a property anymore. And control flow checks on variables like target
do notice the identity of variables. The missing feature is identity blindness specifically when it comes to keys/indexers, not all values everywhere.
And, for what it's worth, copying a property to a new variable like target
and then using only that variable is the standard workaround for ms/TS#10530. So until and unless that issue is addressed, you should probably keep doing that.