21.6 noUncheckedIndexedAccess: Safer Array and Object Access
Right, so you’ve turned on strictNullChecks. Good for you. You’ve stopped lying to the compiler about null and undefined, and it’s started to trust you a bit more. But you’ve probably noticed a gaping hole in your defenses large enough to drive a truck through. You can still do this without so much as a peep from the type checker:
const myArray: number[] = [1, 2, 3];
const element = myArray[5]; // TypeScript thinks this is a `number`. It's `undefined`.
console.log(element.toFixed(2)); // 💥 Runtime error: Cannot read properties of undefined
See? Absurd. We told TypeScript the array is full of numbers, so it assumes every single index access returns a number. It’s a lie, and we both know it. This is where noUncheckedIndexedAccess comes in. It’s the compiler’s way of saying, “Okay, fine, you want real safety? Let’s do this.”
When you enable this flag, TypeScript gets a lot more pedantic—and a lot more helpful. It now understands that when you access an array by index or an object by a key type (like string), you might be getting undefined. So it forces you to deal with that possibility.
How It Transforms Your Types
Enabling this flag doesn’t change the types you write; it changes the types TypeScript infers for certain operations. An array type like number[] is now effectively treated as an array that might have undefined in any of its index positions. So the type of myArray[5] becomes number | undefined.
The same logic applies to objects with index signatures:
const phoneBook: { [name: string]: string } = {
"Alice": "123-456-7890",
};
// Without noUncheckedIndexedAccess: string
// With noUncheckedIndexedAccess: string | undefined
const bobsNumber = phoneBook["Bob"];
This is the core of it. The type system finally acknowledges the fundamental truth of these operations: they are not safe by default.
The Necessary Inconvenience of Handling undefined
This is where you start to feel the friction, and that’s the entire point. The compiler will now yell at you for being reckless.
const numbers = [10, 20, 30];
const thirdElement = numbers[2]; // type is (with flag): number | undefined
// Error: Object is possibly 'undefined'
console.log(thirdElement.toFixed(2));
// The correct way: check for existence first
if (thirdElement !== undefined) {
console.log(thirdElement.toFixed(2)); // All good here
}
// Or use the new-ish optional chaining operator (?.)
console.log(thirdElement?.toFixed(2)); // Safely logs "30.00" or undefined
Yes, it’s more code. It’s also code that won’t crash your application at 2 AM. I’d call that a fair trade.
It’s Smart About What It Doesn’t Apply To
Now, before you throw your hands up and declare this unusable, know that the TypeScript team isn’t completely sadistic. The rule has some common-sense exceptions.
Iteration is safe. When you use a for-of loop or forEach, TypeScript knows you’re accessing every element that exists. It doesn’t slap | undefined on every value.
for (const num of numbers) {
console.log(num.toFixed(2)); // num is a `number`, safe and sound.
}
numbers.forEach((num) => console.log(num.toFixed(2))); // Also fine.
It knows about .length and definite assignments. If you check the length first, TypeScript’s control flow analysis can sometimes figure things out.
function getFirstElement(arr: number[]) {
if (arr.length > 0) {
return arr[0]; // Type is still `number | undefined` 😠
}
return undefined;
}
Wait, what? Yeah, this is one of the rough edges. The compiler isn’t quite smart enough to know that arr.length > 0 means arr[0] is safe. This is a case where you might need to use a type assertion to tell the compiler you know what you’re doing (or refactor). It’s not perfect, but it’s catching the 95% of cases where you had no business being so confident.
Best Practices and Pitfalls
- Enable It Early: Trying to add this to a large, existing codebase is a recipe for a thousand errors. Enable it at the start of a new project or when you’re feeling particularly brave and have a lot of coffee.
- Embrace Optional Chaining (
?.) and Nullish Coalescing (??): These operators are your new best friends. They make dealing with these potentialundefinedvalues concise and readable.const value = someArray[99] ?? defaultValue; - Prefer Tuple Types When Possible: If the length of your array is fixed and known, use a tuple type. The compiler knows exactly what’s in each position.
const fixedArray: [string, number] = ["hello", 42]; const first = fixedArray[0]; // string - Consider Helper Functions: Instead of direct index access everywhere, write a safe lookup function. This contains the type assertion or validation logic in one place.
noUncheckedIndexedAccess is the difference between a type system that’s mostly safe and one that is rigorously, annoyingly, beautifully safe about one of the most common sources of runtime errors. It forces a discipline that, once you get used to it, makes your previous code feel terrifyingly loose. It’s the compiler finally refusing to be an accomplice to your bad habits.