cd ../blog
TypeScriptEngineering ProcessBest Practices

Turning On TypeScript Strict Mode in a Live Production Codebase (Without Breaking Everything)

October 14, 20256 min read

We inherited a large TypeScript codebase with strict mode off. Turning it on revealed 400+ errors. Here's how we migrated incrementally, what the errors actually taught us about the code, and why it was worth the three-sprint investment.

The codebase had `"strict": false` in tsconfig. It had been that way since the project was bootstrapped two years earlier by a team under deadline pressure. By the time I joined as Tech Lead, the project had 80k+ lines of TypeScript that were, technically, not really TypeScript — they were JavaScript with type annotations and a very permissive compiler. Running `tsc --strict` revealed 427 errors across 180 files. Here's what we learned and how we fixed it. ## What Strict Mode Actually Enables `"strict": true` is a shorthand for six compiler flags: - `strictNullChecks` — variables can't be null/undefined unless you say so - `strictFunctionTypes` — function parameter types are checked contravariantly - `strictBindCallApply` — bind/call/apply are properly typed - `strictPropertyInitialization` — class properties must be initialized - `noImplicitAny` — every untyped variable must be explicitly annotated - `noImplicitThis` — this in functions must be typed In our codebase, 80% of the 427 errors were from `strictNullChecks` alone. The codebase was riddled with unchecked nullable access. ## The Migration Strategy: Per-File Opt-In Migrating all 427 errors at once was not an option — we had active feature work. The strategy: incremental migration using per-file `// @ts-strict` comments. In TypeScript 5.0+, you can enable strict mode per file with a comment directive at the top. This lets you migrate file-by-file without changing the global tsconfig. We added a lint rule (custom ESLint rule) that: 1. Blocked any new file from being created without the `// @ts-strict` directive 2. Reported a warning for any modified file that didn't have it This meant the codebase trended toward full strict coverage over time, enforced by CI. ## What the Errors Actually Revealed The most common pattern was this: ```typescript // Before strict: function getUser(id: string) { return users.find(u => u.id === id); // return type: User | undefined (but this was ignored) } // Consumer: const user = getUser(id); console.log(user.email); // Runtime crash if user is undefined ``` We had dozens of these. In most cases the fix was a simple null check. But in about 20% of cases, the undefined return was a real bug — the calling code had an incorrect assumption that the record would always exist. Strict mode didn't just add type annotations; it exposed actual logic errors. ## Patterns We Established **Prefer explicit union returns over throwing.** ```typescript // Instead of: function findOrThrow(id: string): User { const user = users.find(u => u.id === id); if (!user) throw new Error("Not found"); return user; } // We standardised on Result types for expected failure: type Result<T> = { ok: true; data: T } | { ok: false; error: string }; ``` **Use `satisfies` for config objects.** The `satisfies` operator (TS 4.9) lets you validate that an object matches a type while preserving the literal type information. Enormously useful for config files. **Discriminated unions for state.** We replaced boolean flag combinations (`isLoading`, `isError`, `data`) with proper discriminated union state types. This eliminated entire categories of impossible state bugs. ## Was It Worth It? Three sprints of incremental migration work, spread across four months. The outcome: - Production null reference errors in our error tracker dropped by ~60% in the two months after full migration - Code review cycles got shorter — reviewers stopped needing to mentally track nullability - Onboarding new engineers to the codebase became faster — the types serve as accurate documentation The investment was real. But for a codebase that was going to be actively maintained for years, it was unambiguously the right call.

$ ls ../