An empty list does not look dangerous until code runs users[0].name. A dictionary looks complete until a key comes from a request, a file, or a configuration that changed. Without a check, TypeScript can treat both reads as if the value were guaranteed to exist.

noUncheckedIndexedAccess changes that contract. When enabled, an indexed read that may miss a value includes undefined. The compiler does not choose the fix for you. It forces the code to choose a guard, fallback, different iteration form, Map, or an invariant you can actually prove.

This article shows the behavior for arrays and string-keyed objects, a minimal fixture checked with TypeScript 5.9.3, and an incremental migration. The option improves static safety in your code. It does not validate JSON received over the network.

Diagram shows arrays and records passing through a check before producing a safe value.

Short answer

  • Enable noUncheckedIndexedAccess alongside strict when indexed reads should express that a value may be absent.
  • Treat array[i] and record[key] as values that may be undefined.
  • Prefer for...of, an explicit guard, or a domain-appropriate fallback.
  • Use ! only when an invariant was established elsewhere and remains verifiable.

What does noUncheckedIndexedAccess change?

The TypeScript noUncheckedIndexedAccess reference says that the option adds undefined to an undeclared property covered by an index signature. The same idea applies to an array position that may be out of bounds. The type now represents the absence that was already possible at runtime.

Without the option, this compiles even when users is empty:

interface User {
  id: string;
  name: string;
}

const users: User[] = [];
const first = users[0];

console.log(first.name);

With noUncheckedIndexedAccess: true, first is User | undefined. The line that reads name must choose what an empty list means:

const first = users[0];

if (first) {
  console.log(first.name);
} else {
  console.log("no user found");
}

The gain is not that the array becomes safer at runtime. It can still be empty. The gain is that the type stops hiding the assumption, so the code makes the decision where it knows whether absence is expected, an error, or a reason to try again.

Why does strict not enable this option?

The TypeScript 4.1 release notes explain that noUncheckedIndexedAccess can produce noise across many files, so it is not part of the group enabled by strict. A project can use strict: true and still treat items[index] as if it always returns an item.

That detail changes how you read tsconfig.json. strict is a good starting point, but it does not promise that every indexed access has been checked. To enable this check, add it explicitly:

{
  "compilerOptions": {
    "strict": true,
    "noUncheckedIndexedAccess": true
  }
}

The option does not turn every type into T | undefined. Declared properties remain known. The effect appears where the key or position is not guaranteed by the type, such as a numeric array index or a string key used against Record<string, T>.

Where do the errors appear first?

The TypeScript FAQ describes this setting as a broad check. It does not try to prove that every access is in bounds. That is why the first compile often points to patterns that looked harmless but depended on data being present.

Read Type with the option Fix that usually communicates intent
users[0] User | undefined check for absence or use an API with an explicit optional result
users[index] User | undefined store the item and narrow that value
usersById[id] User | undefined handle an unknown key or use Map.get with the same contract
path.split("/")[3] string | undefined validate the structure before using the part
map.get(id) already may be User | undefined keep handling absence

A common trap is expecting a bounds check to solve everything:

for (let index = 0; index < users.length; index += 1) {
  users[index].name;
}

The TypeScript release note records that even a loop with index < length can continue to produce an error. The compiler does not turn that test into a permanent proof about a structure that could be mutated. If the index is necessary, read the item, test the result, and only then access its properties.

How do you fix the errors without using ! to silence the compiler?

Choose the fix based on what absence means. When every present item should be processed, for...of removes the index and states that intent directly:

for (const user of users) {
  console.log(user.name);
}

When absence is possible and changes the result, use a guard:

const user = usersById[userId];

if (!user) {
  return { ok: false, reason: "user-not-found" };
}

return { ok: true, user };

A fallback can also be correct, but it needs to belong to the domain. const limit = settings[key] ?? defaultLimit communicates a rule. settings[key]! only asserts that the rule exists without recording it in the code. If someone renames the key, moves initialization, or changes the data source, the assertion will not find the problem.

For dictionaries with many reads and external keys, consider whether Map represents the operation better. Map.get already returns T | undefined, so the absence decision is visible even without this flag. That does not make Map universally better. It means the type matches a collection where the key may not exist.

A good migration does not turn every error into if (!value) return. Classify each read as normal absence, invalid configuration, a value that needs a fallback, or an invariant established by another step. The type becomes honest only when the chosen behavior is honest too.

How do you enable the option without stopping the whole project?

Treat the change as a compiler contract. Before fixing lines, run the current typecheck and record the command used by CI. Then make the change in a configuration or package with a clear boundary.

A small sequence helps:

  1. Enable noUncheckedIndexedAccess in the selected package's tsconfig.
  2. Run npx tsc --noEmit with the same project used by CI.
  3. Group errors into arrays, records, split strings, and library types.
  4. Fix each group with a guard, iteration, fallback, or suitable data structure.
  5. Run tests covering empty lists, unknown keys, and incomplete configuration.
  6. Keep the option enabled in CI so new indexed reads do not restore the assumption.

For this page, I checked a minimal fixture with strict, noUncheckedIndexedAccess, and noEmit using TypeScript 5.9.3. It compiles after narrowing the values read and demonstrates the behavior described in the documentation. This is not a migration benchmark. A real codebase can have errors in dependency types, generated files, tests, and code that mixes JavaScript with TypeScript.

If the package belongs to a monorepo, introduce the option at a project boundary and then move it through dependents. The article about TypeScript project references explains how to separate projects and keep build order explicit. The concern here is different: the build graph organizes compilation, while this option changes the types of reads inside that graph.

When the read is part of a request result, typing fetch success and error results in TypeScript helps separate a transport failure from absence inside the data.

What does the option not check?

noUncheckedIndexedAccess acts during checking of the code you wrote. It does not inspect JSON from the network, confirm that an external key has the expected shape, or stop another layer from passing the wrong value through any. A TypeScript interface does not perform runtime validation either.

When a value crosses an external boundary, read it as unknown and validate its shape before using it. The guide to validating external JSON before using it in TypeScript covers that step. Both protections are useful: runtime validation proves the received value, while noUncheckedIndexedAccess keeps later reads honest.

The option does not detect every problem with dynamic objects either. A key such as __proto__, an inherited-property collision, or an authorization contract needs a runtime decision. Combine types with validation, data structures, and tests that reach the real boundary.

Use unit versus integration tests in Node.js to decide whether the fix needs to prove only narrowing logic or also the source that produces the keys and positions.

Frequently asked questions

Is noUncheckedIndexedAccess part of strict?

No. The TypeScript 4.1 release note explains that the option is not enabled by strict because it can create substantial migration noise. Add "noUncheckedIndexedAccess": true separately when arrays and index signatures should express that a value may be undefined.

Why does for...of often fix the array error?

for...of yields each element that exists in the iteration instead of asking the compiler to prove the result of a numeric index. It does not make an external source trustworthy or prevent mutations outside the loop. It chooses an operation whose contract better matches processing every present item.

Can I use ! after enabling the option?

Yes, but the operator should represent an invariant the program actually established. If a function can only run after required configuration loads, document and test that precondition. For a key, position, or response that can normally be absent, ! removes evidence without fixing behavior.

Does the option replace JSON validation?

No. It checks types and reads in compiled code. JSON, environment variables, and HTTP responses arrive at runtime and need their own validation. Treat the input as unknown, validate its shape, and then keep the TypeScript code protected against missing indexes inside the flow.

Conclusion

Enabling noUncheckedIndexedAccess makes one question explicit: is the value really there? The answer can be a guard, fallback, for...of, Map.get, or a verifiable invariant. The compiler does not impose one policy, but it stops accepting silence as proof.

Start with a small package, run the same typecheck as CI, and fix errors according to what absence means. Do not use ! to turn a data question into type certainty. Keep the boundary clear too: this option improves the code you compile, while external values still need runtime validation.

Sources consulted

AI assistance helped organize research, draft prose, localize versions, and create the illustration. The fixture and technical claims were checked against the cited documentation; the image is explanatory and not a compiler output.