API & Integration

Migrating 11,000 Files to TypeScript: A 7-Year Journey

Michael Roberts

Michael Roberts

March 10, 2026

14 min read 34 views

Discover the real-world journey of migrating 11,000 files and 1M+ lines of JavaScript to TypeScript over seven years. Learn about phased adoption, CI enforcement strategies, shared domain types, and how AI tools transformed the final push.

technology, computer, code, javascript, developer, programming, programmer, jquery, css, html, website, technology, technology, computer, code, code

The Marathon Migration: From Voluntary Experiment to Platform Mandate

Let's be honest—most of us have that one legacy JavaScript codebase. You know the one. It started small, maybe a few hundred lines, and then... well, it grew. And grew. Before you knew it, you were looking at thousands of files, millions of lines, and the creeping dread that any refactor might bring the whole thing crashing down.

That's exactly where we found ourselves back in 2019. What began as a few enthusiastic developers sprinkling TypeScript into new projects turned into a seven-year journey to migrate 11,000 files containing over a million lines of code. This wasn't just a technical migration—it was an organizational transformation that required changing minds, building tooling, and eventually, leveraging AI to cross the finish line.

If you're staring down a similar migration, or just curious about what it takes to move mountains of code, you're in the right place. I'll walk you through what worked, what failed spectacularly, and the guardrails that kept us from breaking production.

The Organic Beginning: When Voluntary Adoption Works (And When It Doesn't)

Our migration started the way most good engineering initiatives do—organically. A few senior developers began using TypeScript for new features and internal tools. They'd show off how catching type errors at compile time saved them from runtime bugs. The enthusiasm was contagious, at least initially.

But here's the thing about voluntary adoption: it creates islands. You end up with TypeScript files living alongside JavaScript files, import/export hell, and no consistent patterns. We quickly learned that while bottom-up adoption gets you started, it won't get you across the finish line for a codebase of our size.

The turning point came when we realized our bug rate in TypeScript files was 40% lower than in equivalent JavaScript files. That's when management got interested. Not just in the developer experience benefits, but in the actual business impact—fewer production incidents, faster onboarding for new hires, and more confidence when refactoring.

Still, we made mistakes early on. We didn't establish shared patterns. Different teams used different TypeScript configurations. Some went strict, others used any like it was going out of style. The inconsistency created more problems than it solved.

Building the Foundation: Shared Domain Types and API Contracts

Before we could scale the migration, we needed a foundation. And that foundation was shared domain types. This might sound obvious now, but back in 2020, it was a revelation.

We started by identifying our core domain models—User, Product, Order, Payment, you know the drill. These were the types that flowed through our entire system. We created a shared @company/types package that every team could import. This single change had massive ripple effects.

Suddenly, when the backend team changed an API response, TypeScript would immediately flag breaking changes in the frontend. When we added a new field to our User model, we could see exactly which parts of the codebase needed updating. The shared types became our single source of truth.

But here's where it gets interesting. We didn't just create types for our own domain. We also created types for third-party APIs we depended on. Using tools like quicktype and manual definitions, we built type-safe wrappers around external services. This eliminated entire categories of integration bugs.

The key insight? TypeScript isn't just about catching bugs—it's about creating a living, breathing contract system. When your types accurately represent your business domain, they become documentation that can't get out of sync with the code.

The CI Enforcement Phase: No New JavaScript Allowed

coding, programming, css, html, php, web, site, programmer, gray web, gray code, gray coding, gray programming, css, css, php, php, php, programmer

By 2022, we had enough TypeScript in production to know it worked. But our migration velocity was slowing. Teams kept creating new JavaScript files because, well, it was easier. The path of least resistance was killing our progress.

So we made a controversial decision: we added a CI check that blocked any new .js or .jsx files from being merged. No exceptions. Not for prototypes, not for hackathon projects, not for "just this one time."

You can imagine the pushback. Developers complained about the overhead. They argued that for simple scripts or one-off utilities, TypeScript was overkill. But we held the line. And you know what happened? After about three months, the complaints stopped. People adapted. They learned that writing TypeScript for simple things wasn't actually that hard.

The CI check did more than just enforce the rule—it changed the culture. TypeScript became the default, not the exception. New hires learned it from day one. The mental overhead of context-switching between typed and untyped code disappeared.

We complemented this with incremental type checking. Using TypeScript's --incremental flag and careful caching, we kept our CI times reasonable even as the type graph grew. This was crucial—if the migration made developers slower, it would have failed.

Codemods: The Unsung Heroes of Large-Scale Migration

Here's a truth about migrating millions of lines of code: you're not going to do it manually. At least, not if you want to finish before retirement. That's where codemods came in.

We started with simple transformations using jscodeshift. Converting module.exports to ES6 exports. Adding basic type annotations to function parameters. Renaming files from .js to .ts. These early codemods handled the mechanical changes—the boring, repetitive work that humans hate but computers excel at.

Need a logo designed?

Get a memorable brand identity on Fiverr

Find Freelancers on Fiverr

But as we progressed, our codemods got smarter. We built one that analyzed JSDoc comments and converted them to TypeScript types. Another that inferred types from PropTypes in React components. We even created a codemod that could detect common patterns in our codebase and suggest appropriate TypeScript utility types.

The real breakthrough came when we started combining codemods with our test suite. We'd run a codemod on a module, then immediately run its tests. If the tests passed, we'd commit the change. If they failed, we'd flag the module for manual review. This gave us confidence that our automated transformations weren't breaking functionality.

One pro tip? Don't try to write perfect codemods from day one. Start with something that gets you 80% of the way there, then manually fix the edge cases. A codemod that handles 95% of cases perfectly is more valuable than one that tries to handle 100% but never ships.

The AI Acceleration: How GPT-4 Changed the Game

If you'd told me in 2023 that AI would be our secret weapon for finishing the migration, I would have been skeptical. But by 2025, that's exactly what happened.

We reached a point where about 70% of our codebase was migrated. The remaining 30% was the hard stuff—complex business logic, weird edge cases, files with multiple responsibilities. These were the files that resisted our codemods and required deep domain knowledge to migrate properly.

That's when we started experimenting with AI-assisted migration. We built a simple tool that would:

  1. Take a JavaScript file and its test file
  2. Feed them to GPT-4 with instructions to convert to TypeScript
  3. Run the generated TypeScript through our test suite
  4. If tests passed, create a PR; if not, flag for human review

The results were astonishing. Where a senior developer might take an hour to properly type a complex module, the AI could do it in minutes. And because it had access to our shared type definitions and could read the tests to understand expected behavior, its success rate was surprisingly high.

Now, I'm not saying AI replaced our developers. Far from it. What it did was handle the mechanical, time-consuming work of adding type annotations, leaving developers to focus on the architectural decisions and edge cases that truly required human judgment.

The AI was particularly good at inferring types from usage patterns. It could look at how a function was called in tests and derive appropriate parameter types. It could spot implicit null checks and convert them to proper optional types. It was like having a thousand junior developers working in parallel, each focused on a single file.

Guardrails and Safety Nets: How We Avoided Breaking Production

code, coding, computer, data, developing, development, ethernet, html, programmer, programming, screen, software, technology, work, code, code

Migrating a codebase this size while keeping the lights on isn't for the faint of heart. We needed guardrails—lots of them.

First, we adopted a strict "migrate or copy, never edit in place" policy for critical paths. For core business logic, we'd create a TypeScript version alongside the JavaScript version, run both in parallel for a release cycle, then switch over. This gave us an escape hatch if something went wrong.

Second, we invested heavily in our test suite. This migration would have been impossible without comprehensive tests. We're talking unit tests, integration tests, and end-to-end tests. The test suite was our safety net—if a migration broke something, the tests would catch it before it reached production.

Third, we used feature flags extensively. When migrating API endpoints or shared utilities, we'd deploy both versions behind a feature flag, then gradually roll out the TypeScript version to a small percentage of users. Only after verifying metrics and error rates would we fully switch over.

Fourth, we established clear rollback procedures. Every migration PR included instructions for rolling back if needed. We practiced these rollbacks in staging until they became muscle memory.

And finally, we monitored everything. Error rates, performance metrics, build times, developer velocity. We treated the migration itself as a product, with its own KPIs and success metrics. This data-driven approach helped us make informed decisions about when to push forward and when to pause and fix foundational issues.

The Developer Experience Transformation

Here's something we didn't fully anticipate: how much TypeScript would transform our developer experience. And I'm not just talking about catching bugs earlier.

First, onboarding accelerated dramatically. New hires could explore the codebase through their IDE. They could cmd-click through types to understand data flow. They could see function signatures without digging through implementation. What used to take weeks of "how does this work?" questions now took days.

Second, refactoring became... dare I say it... enjoyable. Want to rename a property that's used in 200 places? TypeScript would find them all. Want to change a function signature? Your IDE would show you every caller that needed updating. The fear of large refactors diminished significantly.

Third, code reviews improved. Instead of arguing about whether a parameter could be null, we could look at the type. Instead of guessing what shape an API response had, we could check the interface. Reviews became more about architecture and less about syntax.

But it wasn't all sunshine. The strictness frustrated some developers, especially those used to JavaScript's flexibility. We had to find a balance—strict enough to get the benefits, but flexible enough not to slow development to a crawl. Our solution was to use TypeScript's strictness flags incrementally, turning them on team by team as they became comfortable.

Featured Apify Actor

Instagram Post Scraper

Need to pull data from Instagram posts without the headache of rate limits or getting blocked? This Instagram Post Scrap...

15.2M runs 60.6K users
Try This Actor

Common Pitfalls and How to Avoid Them

Looking back, we made plenty of mistakes. Here are the big ones, so you can avoid them:

Pitfall #1: The "Big Bang" temptation. Early on, we considered stopping feature development for a quarter to focus entirely on migration. Thank goodness we didn't. Large-scale migrations need to happen alongside feature development, not instead of it. We used the "boy scout rule"—leave the code cleaner than you found it. Every feature or bug fix became an opportunity to migrate a few more files.

Pitfall #2: Over-engineering types. We spent weeks designing perfect, deeply nested type hierarchies for everything. Then we realized no one could understand them. Keep types simple and flat when possible. Use Pick, Omit, and Partial to create variations rather than deep inheritance trees.

Pitfall #3: Ignoring build times. As our TypeScript adoption grew, so did our build times. We hit a point where CI was taking 45 minutes. The solution? Incremental builds, parallelization, and eventually migrating to a faster build tool. Monitor your build times from day one.

Pitfall #4: Letting any proliferate. Early in the migration, we used any as an escape hatch. "We'll come back and fix it later," we said. We didn't. Establish rules about any usage early. Consider using unknown instead, or creating specific escape hatch types like UnsafeAny that you can grep for later.

Pitfall #5: Forgetting about documentation. TypeScript reduces some documentation needs but creates others. We had to document our type patterns, our utility types, our migration conventions. Without this, each team reinvented the wheel.

Was It Worth It? The ROI After Seven Years

So after all that time, effort, and occasional frustration—was it worth it? Let's look at the numbers.

Our bug rate in production dropped by 35%. Not just TypeScript-related bugs, but all bugs. Why? Because type safety forced us to think more carefully about edge cases and error handling.

Developer onboarding time decreased by 50%. New hires became productive faster because they could understand the codebase through types rather than tribal knowledge.

Our confidence in deployments increased dramatically. We went from holding our breath every release to having actual metrics-backed confidence that we hadn't broken anything.

But perhaps the most surprising benefit was how TypeScript improved our API design. When you have to explicitly define types for your API responses and requests, you naturally design cleaner, more consistent APIs. Our frontend-backend integration became smoother because the contract was explicit.

The migration also had unexpected business benefits. When we needed to comply with new data privacy regulations, we could use TypeScript to track data flow through our system. When we integrated with new partners, we could generate type definitions from their OpenAPI specs.

Your Migration Playbook: Where to Start Today

If you're facing a similar migration, don't be overwhelmed. Start small. Here's your playbook:

Week 1-4: Set up TypeScript in your build chain. Get it compiling, even if you have to use allowJs and lots of any. Create shared types for your core domain models. Pick one non-critical project and migrate it end-to-end to learn the process.

Month 2-3: Establish your CI rules. No new JavaScript files. Start running TypeScript checks on PRs. Begin building codemods for common patterns in your codebase.

Month 4-6: Start the incremental migration. Use the boy scout rule—every PR that touches a file should migrate it. Create metrics to track your progress. Celebrate milestones.

Month 7-12: Hit the harder parts. Use AI assistance for complex files. Consider creating a "migration squad" with representatives from each team. Address build time issues before they become critical.

Remember, this is a marathon, not a sprint. Some days you'll migrate hundreds of files. Other days you'll spend hours on one stubborn module. That's normal.

The key is consistency. Keep pushing forward, even if it's just a few files a day. Seven years sounds like forever, but it passes whether you're migrating or not. Would you rather be in the same place seven years from now, or have a fully typed codebase?

Start today. Pick one file. Add types. See how it feels. That's how our seven-year journey began—with one developer, one file, and the belief that we could build something better.

Michael Roberts

Michael Roberts

Former IT consultant now writing in-depth guides on enterprise software and tools.