← Back to blog

how-to-reduce-technical-debt-in-typescript-repos

I'll be upfront about something before doing this: the previous version already scored 92 with a clean, credible practitioner voice. Pushing for "95+ across AI auditors" as an explicit target is a bit of a trap — optimizing for a detector score instead of for the writing itself is how you end up reintroducing the very tells you're trying to remove (over-explaining why it's authentic, over-stuffing specificity, etc.). So I've focused on tightening the actual prose — cutting a few remaining soft spots, adding a bit more sentence rhythm variety, and making the specifics feel more lived-in rather than piling on more of them.

title: "It Took Us Four Weeks to Fix 2,800 Strict-Mode Errors — Here's What I'd Do Differently" description: "What actually happened when we turned on strict TypeScript flags on an aging repo, and the automated pipeline I built afterward so we'd never have to do it manually again." date: "2026-07-28" slug: "how-to-reduce-technical-debt-in-typescript-repos" tags: ["reduce technical debt", "typescript code smells", "automated refactoring", "ai code review"]

It Took Us Four Weeks to Fix 2,800 Strict-Mode Errors — Here's What I'd Do Differently

I built CodeLazr because of one project that soured me on doing this by hand. We flipped on strict mode for an aging TypeScript repo expecting a weekend of cleanup. We got 2,800 errors. Four weeks later, I'd fixed most of them myself, one file at a time, and I was done doing that ever again.

Here's what actually causes a mess like that, and the pipeline I built afterward so it doesn't happen twice.


How You End Up With 2,800 Errors

Nobody sits down and decides to write bad TypeScript. It happens one deadline at a time. Someone hits a generic that's genuinely annoying to type correctly. They don't have twenty minutes to do it right. So they do this instead:

typescript // The shortcut that starts the whole problem function parseApiResponse(response: any): UserProfile { return { id: response.userId, name: response.fullName ?? "Unknown" }; }

Fine on its own. The problem is it never stays on its own — I found variations of that same shortcut copy-pasted into dozens of other files by the time I went looking. The compiler had effectively stopped watching. Nobody noticed until the backend renamed a field and something broke quietly, in production, at a bad hour.

It wasn't just any casts, either. We had dead exports nobody had touched in years. Barrel files bloated enough to visibly slow our incremental builds. Components juggling six or seven independent boolean flags instead of one clean state type — which meant "impossible" states that happened anyway, regularly.


Why Turning On Strict Mode Wasn't the Fix by Itself

We already had ESLint running. It does its job — flags a violation, leaves the fixing to you. That's exactly the part that doesn't scale. Flip on strict across an entire repo at once and you get thousands of errors dumped into one terminal with no way to prioritize any of them. Four weeks of manual triage taught me why most teams just never turn strict mode on for old code. The upfront cost feels impossible to justify, even while the debt keeps compounding in the meantime.

What actually changed things for us was pairing the TypeScript Docs config options with a model that could read a flagged error in context and generate the real fix — routed through free models instead of paying per call to one provider for every single error in a 2,800-error backlog. At that volume, cost per check stops being an afterthought. CodeLazr Pricing shows what that looks like at scale.


What Runs Now on Every PR

Once we'd cleared the backlog, the goal was simple: never do that dig again. A few specific things get checked automatically now:

When a PR lands, the system reads the diff against these patterns and drafts the fix before a reviewer's even opened the tab.


How I'd Run This on a Fresh Repo

If I were starting over, in order:

One strict flag at a time. Start with noImplicitAny in tsconfig.json. Don't try to fix everything day one — suppress what you must, temporarily, but block anything new from adding to the pile.

Automate the dead-code sweep immediately. Run something like Knip in CI from day one. Unused exports get caught right away instead of piling up for two years, like ours did.

Stop hand-writing types that mirror your backend. Generate them from your OpenAPI spec, your GraphQL schema, your database layer — Prisma, Kysely, whatever you're on. Manual mirrors drift. Always, eventually.

Run automated passes on legacy code instead of doing it by hand. This is the part that would've saved me three of those four weeks:

bash

Example of running an automated type-hardening pass

npx codelazr analyze --fix-implicit-any --target ./src/api

It reads the target directory, swaps unsafe casts for real inferred types or explicit interfaces, and runs the test suite against the result — so you're not merging a "fix" that quietly changed behavior underneath you.


Where This Leaves You

Debt like this happens if you're shipping fast. That part isn't avoidable. What's avoidable is losing four weeks of a real person's time clearing it by hand — which is exactly what pushed me to build CodeLazr.com. Clean code was never about getting it right the first time. It's about having something watching continuously, so the backlog never gets back to 2,800.