skip to content

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%

answer

  1. measure each flag before committing
  2. one flag per pull request
  3. the expensive one touches every lookup
  4. assertions clear errors and keep no safety
  5. check-only: emit never changes

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.

solid answer

~50 s

I would measure before committing to anything. Each flag can be enabled on its own from the command line over the existing tsconfig, so you can get an exact error count per flag in minutes and rank the work. Then adopt one flag per pull request, because a mixed rollout makes it impossible to tell which errors came from which rule. `noUncheckedIndexedAccess` is almost always the expensive one — it hits every index-loop read and every dictionary lookup — and its risk is not the volume but the fix quality: if the errors get cleared with assertions, you have paid the cost and kept none of the safety. `exactOptionalPropertyTypes` produces fewer errors but they need per-property judgment about whether the API accepts an explicit `undefined`. `noUnusedLocals` is cheap to fix but hostile mid-edit. Crucially, none of these changes emit, so every step is check-only and trivially revertible.

code

bash · 4 lines
bash
# Size each flag independently before committing to it
npx tsc --noEmit --noUncheckedIndexedAccess
npx tsc --noEmit --exactOptionalPropertyTypes
npx tsc --noEmit --noUnusedLocals

go deeper

for a junior

Know that these flags are opt-in and can be tried one at a time, and that enabling one on an old codebase can produce a very large number of errors at once.

for a middle

Describe measuring each flag's cost with a single command-line run, adopting one per change, and the standard fixes each one demands — guards and fallbacks, or widening an optional property's declared type.

for a senior

Lead the rollout: rank the flags by measured error count, name noUncheckedIndexedAccess as the expensive one, set the review norm that kills the assertion shortcut, and keep the editor and CI on one configuration.

for a principal

Own the estate-wide policy — which packages must meet which strictness bar, how a new package inherits it, and where a rule belongs in the compiler versus the linter. Weigh the guarantee each flag buys against sustained friction on every future edit.

## Step one: measure, do not guess These are independent booleans, and the compiler will honour a single one passed on the command line over your existing configuration. So the size of each project is knowable before you commit to it: ```bash npx tsc --noEmit --noUncheckedIndexedAccess npx tsc --noEmit --exactOptionalPropertyTypes npx tsc --noEmit --noUnusedLocals ``` Count the errors, and also look at their *shape* — a thousand errors that are all the same index-loop pattern is a different project from a hundred spread across unrelated modules. This ten-minute exercise replaces the usual argument about which flag is "worth it" with numbers. ## Step two: one flag per change Adopt them one at a time, each as its own pull request that both enables the flag and fixes everything it surfaced. A combined rollout is unreviewable: reviewers cannot tell which edit answers which rule, and if one flag has to be reverted the others go with it. One flag per change also lets you stop — a team can decide after two that the third is not worth it, and that is a legitimate outcome rather than a half-finished migration. Where the codebase is split into packages or directories with their own tsconfig, enabling a flag in one low-traffic area first is a cheap way to learn its real cost and to produce examples of the intended fix before it lands everywhere. ## Step three: expect the flags to cost very different things **`noUncheckedIndexedAccess`** is usually the largest by an order of magnitude. Every C-style index loop and every `Record` lookup produces an error, because a length check does not narrow an index and never will. Most fixes are mechanical — hoist the read into a local and guard it, supply a `??` fallback, or switch to `for...of`, which is unaffected — but the volume is real. Its danger is not volume, though; it is the temptation to clear the wave with assertions. An assertion tells the compiler to believe exactly the claim the flag was questioning, so the codebase ends up with the pre-flag guarantees plus punctuation that implies someone verified the access. If the review norm is not established up front — real guard, real fallback, or iteration — the rollout is worse than not doing it. **`exactOptionalPropertyTypes`** produces far fewer errors, but they resist automation. Each one asks a design question: does this property genuinely accept an explicit `undefined`, in which case widen the declaration, or should the key simply not be written, in which case fix the construction site. No codemod can answer that, so budget reviewer attention rather than volume, and expect the errors to cluster in options-merging and update-style code. **`noUnusedLocals`** is nearly free to fix and the most annoying to live with. Delete a call while debugging and the build breaks on the now-unused import. There is also a trap worth knowing concretely: the leading-underscore convention exempts unused *parameters* under `noUnusedParameters` and unused bindings in array destructuring, but a plain unused *local* named `_thing` is still reported. Teams that assume the underscore is a universal opt-out get surprised. This is the flag most reasonably delegated to a linter, where it can be a warning with an autofix rather than a build failure. ## Step four: keep one configuration The editor and CI must read the same tsconfig. If a flag is enabled only in a CI-specific configuration, developers meet the errors at the worst possible moment and lose the fast feedback that makes the fixes cheap. If you must stage adoption, stage it by *scope* — which files a stricter configuration covers — not by *environment*. ## The reassurance that makes this easy to sell None of these flags changes a byte of emitted JavaScript. TypeScript erases its type layer, and none of these options injects a runtime check or removes a declaration. The rollout therefore cannot alter production behaviour on its own; the only behaviour changes are the ones you deliberately make while fixing errors. That is the argument that gets the flag turned on, and it is also the reason to review the fixes carefully — the risk lives entirely in the edits, not in the configuration. ## What a good answer sounds like Measure, sequence, one flag per change, name the expensive one and why, name the failure mode of a fast fix, and state that emit is unchanged. A weak answer flips everything on at once and reports the error count as an achievement.

  • How do you stop a large noUncheckedIndexedAccess rollout from turning into a wave of assertions?
    Decide the acceptable fixes before the work starts and say so in the pull request description: hoist and guard, `??` fallback, or switch to `for...of`. Then review for them. It also helps to do the first module yourself as a worked example. An assertion re-asserts the exact claim the flag questioned, so a diff full of them means the team paid the whole cost and kept none of the benefit.
  • Would you enable noUnusedLocals in tsconfig or leave unused-code detection to the linter?
    Usually the linter, for developer flow. As a compiler option it is an error that breaks the build the moment you comment out a line, with no autofix and no severity dial. A linter can warn instead of failing, fix on save, and distinguish cases like unused function arguments. Keep tsconfig for the flags that concern type correctness, where a hard failure is the right response.
  • Is it reasonable to enable a strictness flag only in the CI configuration and not locally?
    No — that is the worst arrangement. Developers write code against one set of rules and discover a different set in the pipeline, so feedback arrives at the most expensive moment and the fixes are made under time pressure. If you need to stage adoption, stage it by scope: a stricter configuration covering the directories that are already clean, with the same rules in the editor and in CI.

saying these in an interview costs you the question

  • Enables every flag in one pull request and calls it done
  • Reports the error count as progress without fixing quality
  • Clears noUncheckedIndexedAccess errors with blanket assertions
  • Enables flags in CI only, not in the editor configuration
  • Assumes turning these on can change production behaviour

context