skip to content

Correctness Flags Beyond strict

You will learn the opt-in flags strict does not include: indexed access that admits undefined, exact optional properties, index-signature access rules, and unused or unreachable code checks. Interviewers ask because these are the flags a senior engineer turns on deliberately, and each one changes how you are forced to write code.

part ofTypeScriptoverview, primer and where to startread it →
on this pageshow

questions

5

In a TypeScript tsconfig.json, does setting "strict": true turn on every correctness check the compiler offers? Name checks it leaves off and explain why they are separate options.

level: juniorimportance: should knowfreq 48%

answer

  1. one umbrella, not every check
  2. soundness core versus opt-in extras
  3. the noisy checks are opt-in
  4. noUncheckedIndexedAccess must be listed by name

basics

~10 s

No. strict switches on one fixed family of type-soundness options; a separate set of correctness checks stays off until you list each one by name, including noUncheckedIndexedAccess, exactOptionalPropertyTypes, noImplicitOverride, noImplicitReturns, noFallthroughCasesInSwitch and noUnusedLocals.

solid answer

~40 s

No — `strict` is an umbrella over one fixed family of options aimed at type soundness, and TypeScript keeps several other correctness checks outside it. The ones you opt into by name are `noUncheckedIndexedAccess` (indexed reads admit `undefined`), `exactOptionalPropertyTypes` (an optional property that is present must hold a real value), `noPropertyAccessFromIndexSignature` (dot access reserved for declared properties), `noImplicitOverride` (`override` must be written), `noImplicitReturns`, `noFallthroughCasesInSwitch`, and the unused-code pair `noUnusedLocals` / `noUnusedParameters`. They sit outside `strict` because they are noisier and more opinionated: turning them on usually forces real code changes rather than just adding annotations, so they are left as a deliberate opt-in. In practice `strict: true` is the floor a new project starts from, and a team layers these on one at a time as the codebase can absorb them.

go deeper

for a junior

Be ready to say plainly that strict is an umbrella over one family of options, not every check, and to name two or three that stay off — noUncheckedIndexedAccess and noUnusedLocals are the easiest to remember.

for a middle

Explain what each opt-in flag actually changes about checking, and that all of them are check-only: the emitted JavaScript is identical either way. Knowing that these are tsconfig options rather than lint rules is the discriminator here.

for a senior

Show judgment about adoption order and cost on a real codebase: which flag produces the biggest error wave, which fixes are mechanical, and how you keep the editor and CI reading the same configuration so errors never appear only in the pipeline.

for a principal

Own the policy question of where a rule should live — compiler error versus lint warning — and what that choice does to build failure rates and developer flow. Argue for a strictness floor every package inherits and a documented path for raising it.

## `strict` is a shorthand, not a mode In tsconfig.json, `strict` is one option that switches on a fixed family of related options in a single line. It is a shorthand for listing that family individually; the compiler still tracks each member as its own option, which is why you can set `strict` and then name one family member explicitly to change just that one. What matters here is the boundary: the family it covers is aimed at *soundness of the type model* (things the checker would otherwise be silently guessing about), and TypeScript deliberately keeps a second set of correctness checks outside it. As of TypeScript 6.0 that split is unchanged. ## The checks that stay off Each of these is an independent boolean in `compilerOptions`, and each stays `false` no matter what `strict` is set to: - **`noUncheckedIndexedAccess`** — a read through an array index or an index signature gains `| undefined`, so `rows[0]` on a `string[]` is `string | undefined`. - **`exactOptionalPropertyTypes`** — `retries?: number` stops silently meaning `number | undefined`; a *present* property must hold a real `number`, and writing `{ retries: undefined }` is an error unless you declare `retries?: number | undefined`. - **`noPropertyAccessFromIndexSignature`** — a key that exists only through an index signature must be read with brackets. Given `type Env = { [k: string]: string }`, `env.HOME` errors and `env["HOME"]` is required, so dot access stays a signal that the property was actually declared. - **`noImplicitOverride`** — a member that redefines a concrete base-class member must be written with the `override` keyword. - **`noImplicitReturns`** — a function where some code paths return a value and control can also fall off the end is an error. - **`noFallthroughCasesInSwitch`** — a `case` clause that contains statements and then falls into the next clause is an error. Empty grouped labels (`case 'a': case 'b':`) are still fine, because nothing falls through them. - **`noUnusedLocals`** and **`noUnusedParameters`** — a local or parameter that is declared and never read is an error. - **`allowUnreachableCode`** and **`allowUnusedLabels`** — left unset, unreachable code and unused labels surface only as editor hints; setting them to `false` promotes them to errors. ```json { "compilerOptions": { "strict": true, "noUncheckedIndexedAccess": true, "exactOptionalPropertyTypes": true, "noPropertyAccessFromIndexSignature": true, "noImplicitOverride": true, "noImplicitReturns": true, "noFallthroughCasesInSwitch": true, "noUnusedLocals": true, "noUnusedParameters": true } } ``` ## Why the split exists Two different kinds of pickiness are involved. The `strict` family mostly closes holes where the checker had *no information* — it makes the compiler stop assuming a value can never be null or that an unannotated parameter is fine. The beyond-strict flags mostly disagree with code the author already believes is correct: your loop really does index inside bounds, your options object really does pass `undefined` for a default, your unused import really is about to be used. That is why they are noisy, and why turning them on is a code-change project rather than an annotation-adding project. Keeping them out of `strict` means a codebase can commit to `strict` without also committing to that project. There is also a subtler reason for `noUncheckedIndexedAccess` and `exactOptionalPropertyTypes` specifically: both make the type model *more accurate* at the cost of forcing a check at every use site, and whether that trade pays depends on the codebase. A service that reads a lot of dictionaries benefits enormously; a codebase full of fixed-shape objects mostly gets friction. ## They change checking, never output All of these are check-only. TypeScript erases its type layer at emit, and none of these flags inserts a runtime guard, strips an unused declaration, or alters the JavaScript in any way — they only decide which programs the compiler accepts. That makes each one cheap to try: flip it, look at the error count, and revert if the team is not ready. It also means `noUnusedLocals` is not dead-code elimination; the unused declaration is still emitted, you are simply told about it. ## The boundary people get wrong Unused code, fallthrough cases and missing returns are the classic province of a linter, so candidates often assume these are lint rules rather than compiler options. They are compiler options, they produce ordinary type-check errors, and they fail `tsc` and therefore CI. Deciding whether the compiler or the linter should own a given rule is a real team decision — but it is a decision, not a fact about where the check lives.

  • If these checks are so valuable, why not enable all of them on day one of every project?
    On a greenfield project you largely can — the error count starts at zero, so the cost is only the coding style they force. The calculus changes on an existing codebase, where one flag can produce thousands of errors at once. `noUnusedLocals` is also genuinely hostile mid-edit: comment out a call and your build breaks on the now-unused import, which is why some teams delegate that particular check to the editor or the linter instead.
  • How would you find out, for a given TypeScript version, exactly which options `strict` covers?
    Do not trust memory. `npx tsc --init` writes a commented tsconfig template, and the official tsconfig reference marks each option's relationship to `strict`. Empirically you can also settle it in seconds: write a file that trips the check, compile it with plain `--strict`, and see whether the error appears without naming the flag.
  • Is `noUnusedLocals` a good fit for a compiler flag at all, or does it belong in a linter?
    Both work; the difference is severity and timing. As a compiler flag it is an error that fails `tsc` and blocks CI, which is strong but blunt. A linter can make it a warning, offer autofix, and distinguish cases such as unused function arguments. Many teams keep the type-correctness flags in tsconfig and push code-hygiene rules to the linter so a half-finished edit does not break the type-check.

saying these in an interview costs you the question

  • Thinks strict:true enables every check the compiler offers
  • Assumes noUncheckedIndexedAccess is part of the strict family
  • Calls the unused-code flags linter rules, not compiler options
  • Believes these flags change the emitted JavaScript
  • Cannot name a single correctness option outside strict

context

open as a page

In TypeScript, what does the exactOptionalPropertyTypes compiler option change about a property declared retries?: number, and how do you keep allowing an explicit undefined?

level: middleimportance: should knowfreq 30%

basics

~20 s

exactOptionalPropertyTypes separates absent from present-and-undefined. With it on, retries?: number means the property may be missing but must hold a real number when present, so assigning { retries: undefined } is an error; declare retries?: number | undefined to allow both.

open as a page

In TypeScript, what does the noImplicitOverride compiler option require, which class members does it apply to, and what bug is the override keyword designed to catch?

level: middleimportance: should knowfreq 28%

basics

~20 s

noImplicitOverride requires the override keyword on any member that redefines a concrete member inherited from a base class, including properties and accessors. It catches renamed or deleted base members, whose overrides would otherwise silently become brand-new members.

open as a page

In TypeScript, what does the noUncheckedIndexedAccess compiler option change about the type of rows[0] for const rows: string[], and which accesses does it deliberately leave alone?

level: middleimportance: should knowfreq 52%

basics

~10 s

With noUncheckedIndexedAccess on, every read through an array index or an index signature gains undefined: rows[0] is string | undefined rather than string. Declared properties, fixed tuple positions and for...of iteration are unaffected.

open as a page

Your team wants to adopt TypeScript's beyond-strict correctness flags — noUncheckedIndexedAccess, exactOptionalPropertyTypes and noUnusedLocals — on a large existing codebase. How would you sequence the rollout, and which flag do you expect to cost the most?

level: seniorimportance: should knowfreq 38%

basics

~20 s

Measure each flag's error count first by enabling it alone on the command line, then adopt one flag per change. Expect noUncheckedIndexedAccess to dominate, since it touches every array index and dictionary lookup, and watch that its fixes are real checks rather than assertions.

open as a page