noUncheckedIndexedAccess
Adds undefined to every read through an index signature or numeric array index.
"noUncheckedIndexedAccess": boolean // Default: false- Added in TypeScript 4.1
Parameters
| Parameter | Type | Description |
|---|---|---|
$noUncheckedIndexedAccessoptional | boolean= false | When true, undefined is added to the type of any read through an index signature, including array and tuple element access by a non-literal index. |
Errors
Nothing at runtime; it reports errors such as 18048, 'X is possibly undefined', at the point of use.
Changelog
- PHP 4.1Introduced. It is not part of the strict family and must be enabled by name.
An index signature says what a key maps to if it is there, but nothing guarantees it is. By default TypeScript hands you the value type anyway, which is why arr[i] is typed number even when i is past the end. noUncheckedIndexedAccess adds | undefined to those reads, so the compiler makes you handle the miss.
Where it applies
- Objects with a string or number index signature, including
Record<string, T> - Array and tuple element access with a non-literal index
- Both dot and bracket access on an index-signature type
It does not apply to explicitly declared properties, to tuple access at a literal index, or to writes — arr[i] = 1 is unaffected.
What trips people up
Two things. A length check does not narrow: if (i < arr.length) arr[i].foo still errors, because the compiler does not relate i to length. And for...of, map, filter and destructuring with defaults are unaffected — switching a classic for loop to for...of removes most of the noise. It is not in strict, so you must name it.
Example
// "noUncheckedIndexedAccess": true
const scores: number[] = [10, 20, 30];
const third = scores[2]; // number | undefined (was: number)
const fourth = scores[3]; // number | undefined
console.log(third, fourth); // 30 undefined
interface EnvironmentVars {
NAME: string;
[propName: string]: string;
}
const env: EnvironmentVars = { NAME: "app" };
console.log(env.NAME); // app (declared: still string)
console.log(env.NODE_ENV ?? "(unset)"); // (unset) (string | undefined)
console.log((fourth ?? 0) + 1); // 1When to use it
- Catching an off-by-one array read before it becomes a 'cannot read property of undefined' crash
- Forcing a fallback when reading an environment variable or config key that may not exist
- Making Record<string, T> lookups admit that the key might be missing
Frequently asked questions
Why is my array element possibly undefined?
Because noUncheckedIndexedAccess is on, which adds | undefined to every read through a non-literal index. Guard the value (const x = arr[i]; if (x === undefined) …), use a destructuring default, or iterate with for...of instead.
Does strict include noUncheckedIndexedAccess?
No. It has never been part of the strict family, so it must be listed explicitly in compilerOptions even when strict is true.
Why does checking i < arr.length not remove the error?
TypeScript's control-flow analysis does not connect the index variable to the array's length, so the read is still typed as possibly undefined. Narrow the element itself after reading it, or use an iteration form that yields values directly.
More examples
Why the length check is not enough
Prints A, C, (none), (none). The guard on the value rather than the index is what satisfies the flag — and it also covers the negative index, which a length check alone would let through.
const rows = ["a", "b", "c"];
function at(i: number): string {
// if (i < rows.length) return rows[i].toUpperCase();
// still error 18048: the compiler does not link `i` to `length`.
const row = rows[i];
return row === undefined ? "(none)" : row.toUpperCase();
}
console.log(at(0)); // A
console.log(at(2)); // C
console.log(at(3)); // (none)
console.log(at(-1)); // (none)Loops and destructuring are unaffected
Prints "a 10", "b 20", 0, 5. Object.entries and destructuring defaults sidestep the flag entirely, which is why rewriting index loops as iteration usually removes most of the new errors rather than adding guards everywhere.
const totals: Record<string, number> = { a: 1, b: 2 };
// for...of gives you the value directly: no undefined added.
for (const [key, value] of Object.entries(totals)) {
console.log(key, value * 10);
}
// Bracket access does add it:
const missing = totals["c"]; // number | undefined
console.log(missing ?? 0); // 0
const [first = 0] = [5]; // default handles the miss
console.log(first); // 5
Discussion