Lesson 52 of 80
By default, `arr[i]` is typed as T, full stop — even though arrays don't actually guarantee an element exists at any given index. A flag exists to make TypeScript honest about that gap.
11 min read
Every option explains itself — why the right one is right, and why each wrong one is wrong. Miss one and it comes back later on a spaced-repetition schedule.
This topic is about judgment and practical tradeoffs more than a single right answer — a mock interview is the better way to test yourself on it.
Be interviewed on this →Hints without spoilers, and a plain explanation of why a wrong answer was wrong — answered only from this lesson. Free account, no card.
Sign up freeWhat interviewers actually ask about this, and what a strong answer has to cover.
A strong answer covers
Likely follow-ups
function getFirst(items: string[]): string {
return items[0]; // typed as `string` by default — no complaint
}
getFirst([]); // compiles fine, returns undefined at runtime
items[0] is typed as plain string, not string | undefined, even though items could be an empty array. TypeScript's default indexing rule trusts that if T[] says "elements are string," then any index you write in must produce a string — it doesn't track array length, so it can't actually know whether index 0 exists. This is a real, structural gap in the type system's honesty about arrays, not a bug — arrays just don't carry their length in their type.
noUncheckedIndexedAccess: true closes it: every indexed access into an array (or a Record, or a signature-indexed object type) gets | undefined added automatically.
// with the flag on:
function getFirst(items: string[]): string {
return items[0]; // ❌ Type 'string | undefined' is not assignable to type 'string'
}
function getFirstSafe(items: string[]): string | undefined {
return items[0]; // ✅ the return type now honestly reflects reality
}
const statusMessages: Record<number, string> = {
200: "OK",
404: "Not Found",
500: "Server Error",
};
function describe(code: number): string {
const message = statusMessages[code]; // any number is a valid Record<number, string> key
return message.toUpperCase(); // 💥 crashes if code is, say, 418
}
Without the flag, statusMessages[code] is typed as plain string — Record<number, string> promises every number key maps to a string, which is never actually true for a literal object with three entries. Calling describe(418) compiles cleanly and crashes at runtime on message.toUpperCase(). With noUncheckedIndexedAccess on, message is typed string | undefined, and .toUpperCase() becomes a compile error until you actually handle the missing case — forcing the exact check that would have prevented the crash.
.at() doesn't sidestep this — items.at(0) is already typed string | undefined by the standard library regardless of this flag, because Array.prototype.at was designed after the missing-index problem was well understood. The flag is specifically about closing the gap in plain bracket indexing.for...of loop or .map() callback doesn't need this protection — those give you elements TypeScript already knows exist, one at a time. The risk is specifically in manual indexing (arr[i], record[key]) where the index itself isn't guaranteed to correspond to a real element."Is arr[i] always type T for arr: T[]? Should it be?" is a good probe for whether someone has actually hit an out-of-bounds crash that TypeScript let through — the honest answer is that the default behavior is a known, real gap, and noUncheckedIndexedAccess is the specific, well-known fix for it.