Using VSCode/Typescript. noUncheckedIndexedAccess
is enabled in TS Config.
I am trying to confirm the presence of values in a Float32Array
in a for-loop, and reference them immediately. However, if I used the counter declared in the for-loop to validate the value at an index, and then reference it immediately after, it doesn't seem to acknowledge the validation. e.g.:
const collection = new Float32Array(10);
for (let idx = 0; idx < collection.length; idx++) {
if (collection[idx] !== undefined) {
collection[idx] = collection[idx] * 2;
}
}
Would give me a "Object is possibly undefined
" for collection[idx]
, despite the check just before.
However, if I were to reassign idx
to another value within the loop, and use that as the index instead, it doesn't complain:
const collection = new Float32Array(10);
for (let idx = 0; idx < collection.length; idx++) {
const localIdx = idx;
if (collection[localIdx] !== undefined) {
collection[localIdx] = collection[localIdx] * 2;
}
}
This workaround feels unnecessary, though. Is this a legitimate error, or a bug in the validation tools? Thanks for any insight!
Let's examine a different scenario with the same root cause. This one doesn't depend on noUncheckedIndexedAccess
:
function f(collection: (string | number)[], idx: number) {
if (typeof collection[idx] === 'number') {
const error: number = collection[idx];
// ^^^^^
// Type 'string | number' is not assignable to type 'number'.
// Type 'string' is not assignable to type 'number'.
}
idx = idx + 1;
}
Silly TypeScript; we just proved that collection[idx]
is a number
!
If we don't reassign idx
, the error goes away:
function f(collection: (string | number)[], idx: number) {
if (typeof collection[idx] === 'number') {
const notAnError: number = collection[idx];
}
}
As explained in microsoft/TypeScript#57847, narrowing with a variable index only applies to:
obj[key]
wherekey
is aconst
variable, or alet
variable or parameter that is never targeted in an assignment
"Never targeted in an assignment" is checked conservatively. Narrowing doesn't happen even if assignment occurs in another scope after we've accessed collection[idx]
, nor when idx
is a potentially-reassignable let
/var
binding in global scope, even with no explicit reassignment (because in that case it could be reassigned elsewhere, maybe even somewhere TypeScript doesn't know about).
To drive the point home, here's a case where narrowing would obviously be unsound:
if (typeof collection[idx] === 'number') {
idx = idx + 1;
// Now `collection[idx]` isn't the same element we checked above:
const oops: number = collection[idx];
}
To specifically answer your question:
Is this a legitimate error, or a bug in the validation tools?
I'd say neither.
A human reading the code in your example can see that it's obviously safe (so the error is arguably not "legitimate"), but at the same time this behavior is a known/intended consequence of the current control flow analysis logic (so it's not a "bug"). No matter how smart the type checker gets, if you try hard enough you'll always be able to find cases like this—static type checkers necessarily reject some valid programs due to Rice's Theorem1.
microsoft/TypeScript#61176 proposes making control flow analysis smarter so that when using a literal-typed index, narrowing would always apply (even when the binding is a let
that is reassigned). However, that enhancement wouldn't be enough to address your for
loop use case, as idx
obviously can't be literally-typed as some fixed number there (it's incremented in each loop iteration).
1 Let's forget for a moment that TypeScript's type system is neither decidable nor sound, so Rice's Theorem need not apply.