A half-migrated codebase sets "strict": true in tsconfig.json and tsc reports several thousand errors. How would you get the project to full strict without freezing feature work?
answer
- not one heroic pull request
- umbrella on, members off
- one flag per change
- null checking is the expensive one
- a baseline number CI refuses to raise
basics
~20 sDo not merge the all-at-once change. Stage it: keep strict on but switch individual flags back off and re-enable them one at a time, or scope strict to migrated directories, and make CI hold a baseline error count that is only ever allowed to fall.
solid answer
~50 sThe big-bang pull request is the failure mode, not the goal — it cannot be reviewed, it conflicts with every branch in flight, and it has no rollback smaller than reverting everything. Stage it along one of two axes. By flag: set `"strict": true` and turn individual members back off, then re-enable them one per pull request, cheapest first — `noImplicitAny` before `strictNullChecks`, which is almost always the expensive one. By area: run a second tsconfig whose `include` covers only migrated directories with strict on, while the legacy area stays loose. Whichever axis you pick, freeze the debt in CI: check in the current error count and fail the build if it rises, so new code cannot add errors and the number only ratchets down. Each step is then a config-only change with a one-line revert, which is the rollback story an interviewer is listening for.
go deeper
Know that strict is an umbrella over several checks and that individual ones can be turned off underneath it, so a project can adopt strictness gradually rather than all at once.
Explain the two staging axes — by flag and by directory — and why noImplicitAny is normally enabled before strictNullChecks, whose fixes are design decisions rather than annotations.
Demonstrate the operational half: a checked-in error baseline that CI refuses to let rise, one revertable config change per step, ownership assigned by directory, and a bounded policy for suppressions rather than blanket ones.
Own the definition of done and its cost. Decide whether full strict across the whole tree is worth the engineering time versus a permanently scoped boundary, and make that a stated position with a measurable ratchet rather than an open-ended effort.
## Why the all-at-once change fails Several thousand errors is not a task, it is a project. A single pull request that fixes them all is unreviewable, conflicts with every branch currently open, and takes long enough that it rots. Worse, it has no partial rollback: if it has to come out, everything comes out. The instinct to "just fix it properly" is the instinct to fail. What you want instead is a sequence of small changes, each independently shippable, each independently revertable, with a mechanism that prevents backsliding between steps. ## Axis one: stage by flag `strict` is an umbrella that turns on a family of checks, and individual members can be switched back off underneath it. So the enabling change and the fixing work can be separated: ```json { "compilerOptions": { "strict": true, "strictNullChecks": false, "strictPropertyInitialization": false } } ``` That configuration commits the project to strict while deferring the single most expensive check. (`strictPropertyInitialization` depends on null checking, so it goes off alongside `strictNullChecks`.) Each later pull request deletes one line from that opt-out list and fixes the errors it reveals. Ordering matters. `noImplicitAny` usually comes first — its errors are localised, each fix is an annotation, and the work is mechanical. `strictNullChecks` is normally last within the family, because it is not a syntax change: it forces you to decide, at every site, whether a value can really be absent. Those decisions are design work, and they surface genuine latent bugs, which is exactly why it is worth reaching and exactly why it cannot be rushed. ## Axis two: stage by area The other axis is the file set. Keep the loose configuration for the legacy tree, and run a second tsconfig whose `include` covers only the directories that have been migrated, with `strict` on there. New code lands in the strict area by convention; the boundary moves directory by directory as conversion proceeds. CI runs both checks. The cost is two configurations to keep in sync and a rule the team has to remember about where new files go. The benefit is that the strict area is genuinely strict from day one, rather than everything being uniformly half-checked. The two axes combine. A common shape is: strict-with-opt-outs everywhere, plus a fully strict island that grows. ## The ratchet: make CI hold the line Staging alone is not enough, because a migration that competes with feature work loses. What makes it stick is a ratchet: a checked-in number representing the currently accepted error count, and a CI step that runs the type check, counts errors, and fails if the count exceeds the number. Fixing errors means lowering the number in the same pull request. This is what converts good intentions into a guarantee. New code cannot add type debt, because the build goes red. Progress is visible on a graph rather than in someone's memory. And the migration survives re-prioritisation, because it no longer depends on anyone remembering it. Two refinements are worth mentioning. First, gate new files harder than old ones: a check that fails when a new `.js` file appears in a converted directory is cheap and stops the codebase moving backwards. Second, budget the work explicitly — a directory per sprint assigned to its owning team beats an unowned "migration" ticket that nobody picks up. ## Suppressions: allowed, but bounded Blanket suppression comments sprayed across thousands of lines defeat the purpose: the errors stop being visible, and the comments hide genuine regressions introduced later. If you use suppressions at all, keep them to a bounded set, attach an issue reference, and put a ceiling on the count in CI so it cannot grow. The same discipline applies to casting through `any` to clear an error — that is not a fix, it is the error rewritten as a lie the compiler will believe forever. ## What "done" looks like Define it before you start. Full `strict` across the whole tree is one answer. "Strict everywhere except two legacy directories that are scheduled for deletion" is an equally legitimate answer, provided it is a decision rather than an accident. Announce it, so a permanently mixed codebase reads as the plan rather than as a stalled migration.
- Why is strictNullChecks usually the last of the family you enable?Because its errors are design decisions, not annotations. Every site has to answer whether the value can genuinely be absent, and the honest answer often means changing a signature, adding a guard, or restructuring initialisation. It also uncovers real latent bugs, which is why it is worth reaching — but it cannot be batch-fixed the way implicit anys can.
- How would you stop new code from adding type errors while the migration is still in progress?Make the error count a checked-in artifact and fail CI when it rises. That way any pull request that introduces a new error goes red immediately and the author fixes it while the context is fresh. Pair it with a check that rejects new `.js` files in already-converted directories, so the codebase can only move in one direction.
- When is scoping strict to a subset of directories better than staging by flag?When the legacy area is scheduled for deletion or rewrite, so investing in typing it is waste, or when one team owns a clean vertical slice and can be fully strict immediately. The tradeoff is two configurations to keep aligned and a convention about where new files land, in exchange for an area that is genuinely strict rather than uniformly half-checked.
- Is it acceptable to finish a migration with strict still off in part of the codebase?Yes, if it is a stated decision rather than a drift. Some code is on its way out, and typing it buys nothing. What matters is that the boundary is explicit in configuration, the ratchet prevents the loose area from growing, and the team knows the end state — an undeclared permanent mix is what reads as a failed migration.
saying these in an interview costs you the question
- Fix all the errors in one big pull request
- Suppress everything with ignore comments, revisit later
- Treats strict as an all-or-nothing switch
- Casts through any to clear the error count
- Runs the migration with no CI gate against regressions