The mass migration: moving 200 API endpoints to TypeScript strict mode in one sprint
A production bug from an unchecked null cost us a weekend. I spent the next sprint converting 200 API endpoints to strict mode.
We started FinanceOps with TypeScript in loose mode. No strict null checks, no noImplicitAny, no strictPropertyInitialization. The reasoning was speed: we needed to ship an MVP in eight weeks and type gymnastics would slow us down. That reasoning held up for exactly sixty days. On day sixty-one, a null reference in our payment confirmation handler sent duplicate webhook responses to a client for six hours. The bug was invisible because TypeScript happily let us access a property on a potentially null value without any complaint.
The postmortem was short. The root cause was not the null value. The root cause was that our type system was set to “trust me” mode and we were not trustworthy. I blocked off the next sprint for a single task: enable strict mode across the entire codebase and fix every error.
In that first stretch at FinanceOps, I was still learning how to wear the Head of Engineering title without hiding behind it. It also builds on what I learned earlier in “Why I chose PostgreSQL over MongoDB and how it shaped our entire fintech stack.” The only credibility that mattered was whether the decision survived contact with real money, ugly edge cases, and the next person I would eventually hire. That same bias toward strict boundaries later shaped how I approached ftryos and pipeline-sdk: make correctness boring before you make the API clever.
The Approach: Codemods Over Manual Fixes
With 200 API endpoints and roughly 40,000 lines of TypeScript, manual migration was not an option. I wrote three codemods using ts-morph that handled the most common patterns automatically.
// Codemod 1: Add explicit null checks before property access// Beforeconst name = user.profile.displayName;
// Afterconst name = user.profile?.displayName ?? "Unknown";
// Codemod 2: Add return type annotations to exported functions// Beforeexport function getUser(id: string) { ... }
// Afterexport function getUser(id: string): Promise<User | null> { ... }
// Codemod 3: Replace implicit any parameters// Beforeapp.get("/api/users", (req, res) => { ... });
// Afterapp.get("/api/users", (req: Request, res: Response) => { ... });The codemods handled about 70% of the 1,847 type errors that appeared when I flipped the strict flag. The remaining 30% required manual intervention because they were genuinely ambiguous types that needed a human decision about the correct narrowing strategy.
Three Categories of Bugs Strict Mode Caught
The most valuable outcome was not the migration itself. It was the bugs we found along the way. They fell into three clean categories.
- Null pointer time bombs: 34 places where we accessed properties on values that could be null or undefined. Twelve of these were in payment processing code paths. Any one of them could have caused the same incident that triggered this migration.
- Implicit any leaks: 89 function parameters typed as any that were being passed to other functions expecting specific types. The data flowed through the system without type checking at the boundaries where bugs actually happen.
- Missing return types: 156 exported functions with no return type annotation. TypeScript inferred the types correctly most of the time but the inferred types were wider than intended. A function that should return User | null was inferred as User | null | undefined, which propagated optional chaining through the entire call chain.
The twelve null pointer issues in payment code were the scariest. Each one was a production incident waiting to happen. They had survived for two months because the happy path always provided the values. It was only edge cases, retries, and error recovery paths where the nulls appeared.
The Sprint Timeline
I allocated a full two-week sprint to the migration. Here is how the time actually broke down.
- Day 1: Enable strict mode, run the codemods, commit the automated fixes. Error count dropped from 1,847 to 548.
- Days 2-4: Fix the null pointer issues manually. Every fix required understanding the business logic to choose the right narrowing strategy.
- Days 5-7: Fix the implicit any leaks. Most were straightforward once I traced the data flow.
- Days 8-9: Fix the missing return types and remaining edge cases.
- Day 10: Full test suite pass, manual smoke testing of critical payment flows, deploy to staging.
I finished a day early. The migration touched 312 files across the codebase. Every change was reviewed by reading the git diff twice: once for correctness, once for unintended behavior changes. I was paranoid about introducing regressions in a codebase that processes financial transactions.
Was It Worth It
In the six weeks after the migration, strict mode caught 19 type errors at compile time that would have been runtime errors in loose mode. Four of those were in payment processing paths. At our bug rate before the migration, at least two of those four would have reached production. Each production payment bug costs us roughly a full day of engineering time for investigation, fix, deploy, and customer communication. So the migration paid for itself within the first six weeks.
The builder phase was less glamorous than people imagine. It was mostly a series of stubborn, unfashionable choices that kept future-me out of 2 a.m. incident calls. I still make the same kind of choices inside portfolio, pipeline-sdk, and dotfiles.
Start with strict mode. If you are reading this and your tsconfig still has strict set to false, stop what you are doing and flip it today. The longer you wait, the more expensive the migration becomes.
The migration taught me that type safety is not a technical preference — it is a team scaling strategy. Every endpoint we converted became self-documenting. New engineers could read the type signatures and understand the contract without asking anyone. The investment paid for itself within two months through faster onboarding and fewer integration bugs. If I were starting a new project today, TypeScript strict mode would be enabled before the first line of business logic.