TypeScript noUncheckedIndexedAccess: Safer Array and Dictionary Reads
Enable noUncheckedIndexedAccess, handle possibly missing array and dictionary values, and choose safe narrowing patterns without hiding errors.
noUncheckedIndexedAccess adds undefined to values obtained through unchecked keys or indexes. Turn it on when a dictionary key or array position may be absent at runtime and you want the type to require a check. Declared object properties and known tuple positions keep their precise types.
TypeScript 4.1 introduced the option. It was still separate from strict in TypeScript 5.9, so a project using every strict-family check can still need to enable it explicitly.
Enable the option for the whole project
Add it to the compiler options used by every application and test project:
{
"compilerOptions": {
"strict": true,
"noUncheckedIndexedAccess": true
}
}
The official noUncheckedIndexedAccess reference describes the key change: an undeclared field covered only by an index signature gains undefined.
interface Environment {
APP_NAME: string;
[name: string]: string;
}
declare const env: Environment;
env.APP_NAME.toUpperCase(); // declared property: string
const region = env.DEPLOY_REGION;
// string | undefined
if (region !== undefined) {
console.log(region.toUpperCase());
}
APP_NAME is required by name, so the option does not weaken it. DEPLOY_REGION is accepted only because of the index signature, which cannot prove that this particular key exists.
Array indexes become possibly undefined
An array type promises an element type, but it does not promise that every numeric index is in bounds. With the option enabled, items[0] and items[index] both include undefined:
function firstUpper(items: string[]): string | undefined {
const first = items[0];
return first?.toUpperCase();
}
Do not remove the diagnostic with items[0]! unless some separate invariant truly guarantees a non-empty array. The assertion emits no bounds check. A tuple can encode that invariant instead:
type NonEmpty<Type> = [Type, ...Type[]];
function first<Type>(items: NonEmpty<Type>): Type {
return items[0];
}
Known tuple positions are declared properties, so items[0] remains Type. A computed numeric access can still be out of range and is checked accordingly.
When the index itself is unnecessary, iterate over values. The TypeScript 4.1 release notes recommend for...of or forEach because each visited element is known to exist:
declare const items: string[];
for (const item of items) {
console.log(item.toUpperCase());
}
An index-based loop may still produce string | undefined; TypeScript does not treat index < items.length as a permanent guarantee about a mutable array.
Narrow the lookup result once
Read a dynamic property into a local variable, check it, and use that narrowed value:
type Counts = Record<string, number>;
function increment(counts: Counts, key: string): void {
const current = counts[key];
counts[key] = current === undefined ? 1 : current + 1;
}
This is correct only if undefined cannot be a stored value. For Record<string, number | undefined>, a missing key and a present undefined value have the same result when read. If presence itself matters, use Object.hasOwn, the in operator, or a Map and define what an explicit undefined means.
Defaulting can be concise when the domain has a sensible fallback:
function labelFor(labels: Record<string, string>, key: string): string {
return labels[key] ?? `(unknown: ${key})`;
}
Use ??, not ||, when empty strings, zero, or false are valid stored values.
The option cannot prove external data is valid
Record<string, User> still claims that every present value is a User. The option changes whether a key might be missing; it does not validate values loaded from JSON, a database, or an environment variable. Parse untrusted data before assigning that type.
It also does not make arrays immutable. A callback or another alias can remove an element after an earlier check. Keep the checked result in a local variable when later mutation is possible, or choose an API that makes the invariant explicit.
Expect noise when first enabling the option in code that uses arrays as if they were tuples or dictionaries as if every key were populated. Fix each site according to the real contract: check absence, provide a default, iterate over existing values, or strengthen the type to a tuple or named property. A non-null assertion should be the last choice because it preserves the old runtime risk.