TypeScript strict mode migration: the six-month project I wish I had done in month one
We started with TypeScript loose mode because "we will tighten it later." Eighteen months and 40,000 lines later, migrating to strict surfaced 847 errors, including three that affected financial calculations.
When we started FinanceOps in mid-2023, I made the decision to use TypeScript in loose mode. No strict null checks. No strict function types. No no-implicit-any. The reasoning was that we needed to move fast, and strict mode would slow us down with type errors on code that worked fine at runtime. Eighteen months later, I enabled strict mode on our 40,000-line codebase and the compiler produced 847 errors. Three of them were in financial calculation code. One of those three had been silently producing incorrect results in production for four months.
This is where the homelab stopped being a hobby and started acting like a leadership tool. It also builds on what I learned earlier in “Claude Opus 4 and Sonnet 4: the week AI coding tools stopped being novelties and became infrastructure.” The infrastructure and ctrlpane work gave me a cheap place to pressure-test release habits, GitOps discipline, and failure modes before I asked the team to trust those defaults at work.
The Three Financial Bugs
The first bug was in our fee calculation module. A function accepted an optional discount parameter with a default value. In loose mode, TypeScript allowed the function to be called with undefined without complaint. The function multiplied the base fee by (1 - discount). When discount was undefined, JavaScript evaluated (1 - undefined) as NaN, and the entire fee became NaN. Our UI rendered NaN fees as $0.00 because of a separate display formatting bug that converted NaN to zero. Clients were being undercharged on fees. Nobody noticed because the dollar amounts were small.
// Loose mode: no errorfunction calculateFee(amount: number, discount?: number) { return amount * 0.025 * (1 - discount) // discount could be undefined!}
// Strict mode: Error: Object is possibly 'undefined'// Fixed:function calculateFee(amount: number, discount: number = 0) { return amount * 0.025 * (1 - discount)}The second bug was a null reference in our reconciliation matching score. A database query returned null for a field that TypeScript inferred as number because the column was non-nullable in the ORM type definitions, but the actual query used a LEFT JOIN that could produce nulls. Strict null checks would have caught this at the call site. Instead, the null propagated through three functions before causing an incorrect match score that occasionally paired the wrong transactions together.
The third bug was an implicit any in a utility function that converted currency amounts between formats. The function accepted a value parameter without a type annotation. In loose mode, this was implicitly typed as any, meaning TypeScript performed no type checking on the operations within the function. The function was accidentally called with a string amount in one code path, producing string concatenation instead of numeric addition.
The Incremental Migration Strategy
Enabling strict mode globally on day one would have blocked all development. 847 errors across 40,000 lines of code means roughly 2 percent of lines need changes. That is not a single PR. It is a project. We needed a migration strategy that let us tighten the type system incrementally while continuing to ship features.
- Enabled strict mode in tsconfig.json with skipLibCheck to avoid third-party type issues
- Used the ts-expect-error comment to suppress all 847 existing errors without fixing them
- Added a CI check that counted ts-expect-error comments and failed if the count increased
- Assigned each engineer a target of resolving 20 suppressions per week alongside regular feature work
- Prioritized suppressions in financial calculation code, database query code, and API boundary code
The CI check was the enforcement mechanism. You could not add new code that would be a strict mode violation because strict mode was enabled. You could not increase the suppression count because CI would fail. You could only reduce the count over time. The suppression count started at 847 and dropped by roughly 80 per week.
What Strict Mode Actually Catches
After completing the migration, I categorized every error that strict mode had surfaced to understand what types of bugs loose mode was hiding.
- 412 errors were implicit any types that masked type mismatches in function arguments and return values
- 203 errors were possible null or undefined values used without null checks
- 98 errors were incorrect function signatures where parameter types did not match usage
- 67 errors were unreachable code branches that TypeScript could prove were dead with strict analysis
- 34 errors were type assertions that strict mode identified as incorrect
- 33 errors were miscellaneous including incorrect generics and missing type parameters
The implicit any category was the largest and the most dangerous. When TypeScript infers any, it stops type checking entirely for that value. Every operation on an any value succeeds at compile time regardless of whether it makes sense at runtime. In a fintech codebase where numeric precision matters, having 412 values that bypass type checking is not a style issue. It is a correctness risk.
The Cost of Waiting
The migration took six months from enabling strict mode with suppressions to resolving the last suppression. Six months of engineers spending a few hours per week fixing type errors that should never have existed if we had started with strict mode from day one.
By this stage the job had changed. I was no longer just picking a tool or fixing a bug. I was carrying the blast radius across product, compliance, sales, and hiring. That is exactly why I kept pressure-testing the same lesson inside ftryos and pipeline-sdk.
Start with TypeScript strict mode from day one. Not because it is a best practice. Because the cost of migrating to strict mode on a mature codebase is measured in months, and the cost of the bugs it would have prevented is measured in client trust. There is no scenario where loose mode saves more time than it costs.
If you are starting a new TypeScript project today, enable strict, noUncheckedIndexedAccess, and exactOptionalPropertyTypes in your tsconfig. Yes, it will slow you down for the first week while you learn to write types correctly. After that, it will speed you up because you will spend less time debugging runtime type errors that the compiler would have caught. The four months of incorrect fee calculations we shipped to production cost more in client remediation than strict mode would have cost in development time over the entire life of the project.