Spectre
// PUBLISHED24.10.26
// TIME6 MINS
// TAGS
#TYPESCRIPT#STRICT MODE#MIGRATION#TYPE SAFETY
// AUTHOR
Spectre Command

TypeScript Gotcha #2: Enabling Strict Mode Without Destroying Your Team

Meta: "TypeScript strict mode migration doesn't have to wreck your team. Here's the incremental path that actually finishes without halting feature delivery." (155 chars)


Y

ou add "strict": true to your tsconfig.json. TypeScript responds with 2,341 errors. Your engineers look at the list, don't say anything in standup, and someone reverts it that evening.

I've watched this sink more than one strict mode migration attempt. The errors weren't wrong. The approach was.

TypeScript strict mode is worth enabling. The bugs it surfaces are real null dereferences that crash Node.js in production, type assumptions that only break under load, interface mismatches that were wrong from day one. But dropping strict on a live codebase all at once isn't a migration strategy. It's a way to ensure the migration never happens.

Here's how to actually do it.


What "strict": true Actually Turns On

Most engineers treat strict as a single switch. It's shorthand for eight separate compiler flags.

The two that cause most of the disruption in existing codebases: strictNullChecks, which forces you to handle null and undefined explicitly rather than pretending they don't exist, and noImplicitAny, which bans type inference that resolves to any. These two will surface 80% of the real bugs in most codebases and generate 80% of the error count.

The other six strictFunctionTypes, strictBindCallApply, strictPropertyInitialization, noImplicitThis, alwaysStrict, and useUnknownInCatchVariables are genuine improvements but cause far fewer errors in practice. Most of them are localized. A function signature here, a class property there.

Knowing the breakdown matters because it changes how you sequence the migration.


Why Enabling Everything at Once Always Fails

The error count isn't the problem. 2,000 errors sounds catastrophic, but in a codebase with a consistent pattern say, every function that might return undefined is being used without a null check it's the same mistake repeated across hundreds of call sites. Fix the pattern, not each instance, and the count drops fast.

The problem is the task structure. "Fix all strict errors before merging" is not a sprint ticket. It's a multi-week project competing directly with your backlog. And it has no obvious stopping point. Fix a batch, pull main, find new errors from someone else's feature branch.

Teams that try this end up in one of two places. They spend weeks on type errors while the product backlog piles up, or they give up halfway and commit a codebase that's half-strict. That second outcome is actually worse than either extreme. You can't trust which files are safe, and you've lost the signal that strict gives you on new code.


The Migration Path That Actually Finishes

Two approaches work. Use whichever matches your team's workflow.

The dual tsconfig approach useful when you need strict and the old codebase to coexist without blocking CI:

Create tsconfig.strict.json extending your base config:

Untitled
1{
2 "extends": "./tsconfig.json",
3 "compilerOptions": {
4 "strict": true
5 },
6 "include": ["src/payments/**", "src/auth/**"]
7}

Run it in CI alongside the main build. During migration, let it fail as a warning rather than a blocker. As modules get cleaned up, move their paths into the strict include list. Over time the include grows until it covers everything, then you flip the main tsconfig and retire the strict one.

Your team ships features while the migration runs in parallel. The cost: you need CI discipline to actually keep moving files across, or nothing happens.

The flag-by-flag approach useful when you want clear milestones and measurable progress:

Enable strict flags one at a time in the main tsconfig, in this order:

  1. noImplicitAny first. Fewest surprises, clearest fix name your types.
  2. strictNullChecks second. Most bugs, most work. Takes longest. Worth it.
  3. strictPropertyInitialization third. Class-level fixes, usually contained.
  4. Leave strictFunctionTypes and useUnknownInCatchVariables for last.

Each flag gives you a scoped error list. Assign flags to sprints. Progress is visible. Nobody stares at 2,341 errors with no sense of where to start.

For the compiler options that control all of this, the production tsconfig guide covers every setting worth knowing.


The Part That Derails Most Migrations

Engineers fix the error count without fixing the types.

The pattern looks like this. TypeScript flags a null error. The engineer adds a non-null assertion:

Untitled
1const user = getUser(id)!;

The error disappears. Tests pass. Code ships. But you've told TypeScript "trust me, this will never be null" and if it ever is, you get a runtime crash with no warning. The code isn't safer. The type checker is now blind to an actual bug.

The correct fix handles the null:

Untitled
1const user = getUser(id);
2if (!user) {
3 throw new UserNotFoundError(id);
4}

Or propagate the optionality up the call chain. Or redesign the function so it can't return null in that context. Any of these is better than !.

Non-null assertions aren't always wrong. There are cases where you know something about runtime state that TypeScript can't infer from the types. But during a strict migration, each ! you add is a deferred problem. They accumulate. Add a lint rule @typescript-eslint/no-non-null-assertion that requires a reviewer comment justifying every assertion. That forces the conversation about whether the fix is real or just the error suppressed.


A Real Migration, Numbers and All

A payments team at an Indonesian fintech startup had 40,000 lines of TypeScript built over three years without strict mode. The implicit assumption woven through the codebase: every user object had a paymentMethod field. Set at signup, always present, never checked.

Except for users who signed up during a three-day window when a signup bug silently skipped that step. About 2% of their user base. For those users, the charge function was calling user.paymentMethod.provider and throwing at runtime caught by a broad catch block, logged to a monitoring dashboard nobody watched, surfacing to users as a generic "payment failed."

Enabling strictNullChecks would have flagged the entire call chain as potentially unsafe. Not a one-line error you'd have had to trace the optionality through several functions. But it would have shown up.

They ran the dual tsconfig approach over five weeks. Two engineers focused on the payments domain first, then auth, then everything else. Found eight places where paymentMethod was assumed present without a guard. None of the developers who wrote those call sites thought they were doing anything risky.

That's the thing about null assumptions. They feel safe until they aren't.


FAQ

Q: Does enabling strict mode break existing JavaScript interop? A: Not directly. Strict mode catches TypeScript-level type errors, not runtime behavior. Your JS interop keeps working, but TypeScript will push you to type external module usage more carefully. The @types/* packages handle most of it already.

Q: Should I enable strict on a new project from day one? A: Yes, without hesitation. The cost of backfilling strict compliance grows with codebase size. Enabling it at the start means every new file follows the rules automatically. The TypeScript project setup guide covers how to structure this from scratch.

Q: How does strict mode relate to the any problem? A: noImplicitAny one of the eight flags inside the strict bundle is what tackles implicit any inference. But strict mode doesn't prevent you from writing explicit any if you choose to. That's a separate discipline. The any escape hatch gotcha covers why explicit any is still a problem even with strict fully enabled.

Q: Is it worth enabling strict on a codebase we plan to rewrite? A: Usually not. If the rewrite is genuinely under six months away, put that effort into the new codebase and enable strict from day one there. If "we'll rewrite it eventually" has been the plan for two years, enable strict now and treat your team's reaction to the error count as data about whether the rewrite will actually happen.

Q: We have 5,000 strict errors. Where do we actually start? A: Run tsc --noEmit 2>&1 | grep "error TS" | sed 's/.*error //' | sort | uniq -c | sort -rn | head -20. This gives you the most frequent error codes. The top two or three codes typically account for more than half your total count. Fix the pattern, not the instances.


The migration is grunt work. Not glamorous. But the alternative is a codebase where the type system is lying to you and you won't know it until production tells you.

Most teams that fail at strict migration treat it as one task. Give your team a flag, a folder, and a finish line instead.


Internal links used:

External links used:

  • None required

Word count: ~1,490

// END_OF_LOGSPECTRE_SYSTEMS_V1

Is your current architecture slowing you down?

Stop guessing where the bottlenecks are. We partner with founders and CTOs to audit technical debt and execute zero-downtime system rewrites.

Book an Architecture Audit