Originally published on Hashnode. Cross-posted for the DEV.to community.
The first time I tried to migrate a large JavaScript codebase to TypeScript, I made the classic mistake. I planned a six-week migration project, kicked off with a team meeting, started at the top of the directory tree, and got about 4% through before the project stalled. Other work kept coming in. The migration sat in a long-running branch that diverged from main every day. Three months later, I gave up and merged the partial work back with a lot of any types and a lot of regrets.
The second time I tried, I had learned the lesson. Big-bang migrations do not work in production codebases that have to keep shipping. The migration has to be incremental, and it has to be done in a way that lets the rest of the team keep working without coordination overhead. The tools that exist for this kind of migration are good but require enormous amounts of human time to apply correctly across a large codebase.
This is where Claude Code changed the math for me. I built a migration workflow that turned what would have been a six-month project into a six-week project, and the codebase kept shipping the entire time. Today, the migration is done, the code is fully typed, and the team is faster than they were before. Here is how the workflow works.
Why Most TypeScript Migrations Fail
The reason most TypeScript migrations fail is that they are framed as a project. Projects have start dates and end dates and dedicated resources. Production codebases do not. They have a continuous stream of feature work that cannot stop, and they have a team that cannot pause for weeks to focus on a migration.
When the migration is a project, it competes with feature work for time. Feature work always wins because feature work has external pressure. The migration loses, falls behind schedule, and eventually gets canceled or quietly abandoned.
The migrations I have seen succeed are the ones framed as a continuous activity rather than a project. The work happens alongside feature work. Each commit takes a small bite out of the migration. The bites accumulate. After a few months, the migration is done without anyone having scheduled a migration sprint.
The migration succeeds when it becomes invisible. When the work is so cheap that every PR can include a slice of it without anyone noticing, the migration moves forward at the speed of the regular development cadence. The migration that demands focus is the migration that gets deprioritized.
The challenge is making the work cheap enough. TypeScript migration is not naturally cheap. Adding types to existing code requires understanding the code, the runtime behavior, the patterns of use, and the edge cases. Doing this well across thousands of files takes a long time. Doing this badly produces a codebase full of any types that buys none of the benefits of TypeScript.
The Claude Code workflow makes the work cheap by automating the parts that can be automated and focusing human attention on the parts that cannot.
The Foundation Skill
Before you can migrate any code, you have to set up the foundation. The foundation skill handles the project configuration that makes incremental migration possible.
The skill configures the TypeScript compiler to accept both .ts and .js files. It enables allowJs so existing JavaScript files keep working. It enables checkJs so JSDoc types in JavaScript files get checked. It sets strict mode for new TypeScript files but allows untyped JavaScript files to coexist.
The skill also sets up the build pipeline. The build needs to compile a mix of .ts and .js files. The test runner needs to handle both. The bundler needs to handle both. The CI needs to type-check the TypeScript files and lint everything together. Each of these has small configuration changes that the skill handles in a single pass.
The foundation skill produces a codebase where you can rename a .js file to a .ts file and it still works. That is the precondition for incremental migration. Without it, every renamed file becomes a blocker that breaks the build for everyone.
The Inventory Skill
Once the foundation is in place, the inventory skill maps out what needs to be migrated. The map is the basis for prioritization.
The skill produces an inventory of every JavaScript file in the codebase. Each entry includes the file path, the size in lines, the number of exports, the number of importers, the cyclomatic complexity, and a migration difficulty estimate. The difficulty estimate is based on signals like dynamic property access, runtime type checking, eval usage, and the presence of patterns that are hard to express in TypeScript.
The inventory also includes a dependency graph. For each file, the skill lists which files depend on it and which files it depends on. The graph is what drives the migration order. Files with no dependencies on other JavaScript files are leaves. Leaves can be migrated independently. Files with many JavaScript dependencies are roots. Roots have to wait until the dependencies are migrated.
The output is a prioritized list of migration targets. The top of the list is leaf files with low difficulty and high importance, ranked by impact per hour of work. The bottom of the list is complex roots that depend on many other things being migrated first.
The inventory becomes the migration plan. Instead of asking "what should I migrate next?" I look at the next entry on the list. The list itself is updated as files get migrated, so the next entry is always the right next entry.
The Conversion Skill
The conversion skill handles the actual migration of a single file. The skill takes a JavaScript file and produces a TypeScript file with types added.
The skill starts by reading the file and understanding its structure. It identifies all the exports, the function signatures, the class definitions, the constants, and the patterns of use. It then queries the importers of the file to see how the exports are actually used. The usage tells it what the types should be.
For a function that takes a string and returns a number, the skill can infer the types from the function body if the body is simple enough. For a function that takes an object with various properties, the skill looks at how callers construct the object and what properties they pass.
For exports that are used in multiple places with conflicting types, the skill produces a union type or a generic. The decision depends on the pattern. If the function is genuinely polymorphic across usages, the skill uses a generic. If the function has a few specific usage patterns, the skill uses a union.
The skill avoids any whenever possible. When the type is genuinely unknown, it uses unknown instead, which forces the caller to narrow the type before using the value. When the type is partially known, it uses the most specific type it can derive.
The output is a TypeScript file with types that match the actual usage. The file is not perfect. Edge cases that the skill could not figure out are flagged for review. But the bulk of the work is done.
The Validation Skill
After the conversion skill produces a TypeScript file, the validation skill checks the result. The check has three parts.
The first part is the compile check. The TypeScript compiler runs on the file and reports any type errors. The skill reads the errors and decides whether they are real or whether they reflect places where the inferred types were wrong. The skill can often fix the inferred types automatically if the error is clear.
The second part is the test check. The test suite runs to make sure the migrated file still behaves correctly. If a test fails, the skill correlates the failure with the migration. Most test failures after a migration are caused by overly strict types that rejected runtime patterns the original code allowed. The skill identifies these and proposes a fix.
The third part is the usage check. The skill looks at every importer of the migrated file and verifies that the new types work for them. If an importer was passing an argument that does not match the new type, the skill identifies the mismatch. The mismatch might be a bug in the importer, in which case it should be fixed. Or it might be a sign that the migrated type is too narrow, in which case the type needs to be widened.
The validation skill catches the cases where the migration would have broken something downstream. Without it, a migration that compiles locally can introduce errors that only surface when other files try to use the migrated module. Catching these at migration time is much faster than catching them later.
The Pattern Library
Most JavaScript codebases have repeated patterns. The same idiom for error handling. The same shape of options object. The same approach to async iteration. Once you have migrated one instance of a pattern, the rest of the instances can be migrated mechanically.
The pattern library skill identifies repeated patterns in the codebase and learns how to migrate them. The library starts empty. As migrations happen, the skill notices when a similar pattern appears and asks whether to apply the same migration approach. After a few applications, the pattern is captured and applied automatically.
The pattern library is what makes the migration accelerate over time. The first hundred files are slow because everything is novel. The next hundred files are faster because most of the patterns are already captured. The last few thousand files are fast because almost everything is a known pattern.
The library also handles the codebase-specific idioms. Every codebase has weird things. Custom hooks. Custom decorators. Custom inheritance patterns. The library captures these and applies them consistently across the migration, which means the migrated codebase has consistent type patterns instead of one-off solutions in every file.
The Coordination Skill
The migration happens alongside feature work. Feature work changes files. Migration changes files. When the migration touches a file someone else is also touching, there is potential for conflict.
The coordination skill prevents this. The skill watches the open pull requests across the team and reserves files that are being actively worked on. The migration never touches a file that has an open PR against it. When the PR merges, the file becomes available for migration. When the migration is in progress, the team is notified to avoid that file.
The coordination is what keeps the migration from creating friction for the team. Without it, the migration would be a source of constant merge conflicts. With it, the migration moves through the parts of the codebase that are quiet at any given moment, and the team rarely notices.
The skill also batches the migration into small PRs. Each PR migrates a handful of related files. The small PR size makes the migration changes easy to review and reduces the chance of conflicts. The team reviews the migration PRs the same way they review any other PR, just with the awareness that the changes are mostly mechanical.
The Strictness Ramp
TypeScript has many levels of strictness. Starting with full strict mode in a freshly migrated codebase is too much, because the migration produces types that are correct but not always the strictest possible. The strictness ramp skill increases strictness gradually as the migration matures.
The first level of strictness allows implicit any but checks everything else. The second level disallows implicit any but allows explicit any. The third level disallows any entirely and requires unknown instead. The fourth level enables exhaustive switch checking. The fifth level enables strict null checks. The sixth level enables strict function types. The seventh level is full strict mode.
The skill tracks where the codebase is on each level and surfaces opportunities to advance. When 90% of the files pass a stricter level, the skill suggests turning that level on globally and fixing the remaining 10%. The ramp lets the codebase get to full strict mode in stages instead of trying to satisfy all of strict mode at once.
The strictness ramp also includes per-file overrides. A file that is not yet at the target level has its level set explicitly, and the override is removed when the file reaches the target. This way the codebase can have a mix of strictness levels temporarily while everything converges.
The Regression Detection Skill
A successful migration is one that does not introduce regressions. The regression detection skill watches for cases where the migration changed runtime behavior accidentally.
The skill has three modes of detection. The first mode is type-driven, looking for places where the new types narrowed the behavior compared to what the JavaScript allowed. If the original code accepted a number or a string and the new type only accepts a number, the skill flags it for review.
The second mode is test-driven, watching for tests that started passing or failing after the migration. A test that started failing is an obvious regression. A test that started passing is sometimes a sign that the migration fixed a latent bug, but more often a sign that the test is checking something the migration changed.
The third mode is production-driven, watching for runtime errors that appear after deployment of the migration. The skill correlates production errors with the files that were migrated and surfaces likely regressions. This catches the cases where the type system allowed something the runtime did not, or where the migration introduced a subtle behavior change that only manifests in production.
The regression detection is what makes the migration safe. Without it, a migration that looks good in development can introduce production issues that take weeks to find. With it, regressions are caught quickly and rolled back before they accumulate.
How the Skills Compose
The skills compose into a migration cadence. Each day, the inventory skill identifies the next handful of files to migrate. The coordination skill confirms they are available. The conversion skill produces TypeScript versions. The validation skill checks the result. The regression detection skill watches for issues.
I review the produced PRs, approve them, and merge them. The team reviews them as part of their normal workflow. The pattern library skill captures any new patterns. The strictness ramp skill tracks progress and surfaces opportunities to advance.
The total time I spend on the migration is about 30 minutes per day. That is enough time to review and merge five to ten file migrations. The team spends almost no extra time, because the PRs are small and mechanical.
Over a few months, the migration completes. The codebase moves to TypeScript without anyone scheduling a migration sprint, without anyone feeling like the migration was disruptive, and without any production regressions caused by the work.
What This Costs
The skills took a few weeks to build, mostly because the conversion skill needed a lot of tuning to produce good types instead of any types. The pattern library skill needs a few months of usage to accumulate the patterns specific to your codebase.
The benefit is in the rate of migration. Before this workflow, a TypeScript migration on a 200,000-line codebase would have been a six-month project requiring dedicated headcount. With the workflow, it was a six-week migration that ran alongside normal feature work and required about an hour of my time per day.
The benefit also shows up in the quality of the migration. Codebases that get migrated in a hurry end up with any types scattered throughout, because the team did not have time to do the work properly. Codebases that get migrated with this workflow end up with proper types from the start, because the conversion skill defaults to specific types and only falls back when forced.
What the Skills Do Not Do
The skills do not replace architectural decisions. When the migration reveals a design that does not work in TypeScript, the skills tell you but do not redesign. Some patterns that work in JavaScript do not have clean TypeScript equivalents and require code restructuring. The restructuring is yours to do.
The skills also do not write tests. They check that existing tests still pass and surface places where new tests would be valuable, but they do not write the tests themselves. The test writing is yours.
The skills also do not enforce style decisions. Whether to use interfaces or type aliases, whether to prefer const assertions or explicit types, whether to use enums or string unions, these are style choices the skills are agnostic about. You configure them based on your team's preferences.
Setting Up Your Own Workflow
Start with the foundation skill. Without the foundation, nothing else works. Get the project to a state where renaming a .js file to .ts does not break the build. This is the minimum viable starting point.
Add the inventory skill next. You need to know what you have before you can plan the migration. The inventory tells you whether the migration will take a week or a year.
Add the conversion skill after that. Migrate ten files manually first to see what good output looks like, then build the conversion skill to produce similar output. The first version of the conversion skill will be rough. Tune it on real files until the output is consistently good.
The validation, pattern library, coordination, strictness ramp, and regression detection skills can come later. They are valuable additions but the migration can start without them. Build them as you discover the need.
The Bigger Picture
The pattern in this migration workflow is the pattern I keep seeing in every successful application of Claude Code to a large engineering problem. The work has repetitive parts and judgment parts. The repetitive parts can be automated. The judgment parts cannot. The automation is what makes the work tractable. Without the automation, the work is too expensive to do well. With the automation, the work becomes routine.
TypeScript migration is a particularly good example because the scale is so visible. A 200,000-line codebase is intimidating. Most of the lines do not require any human judgment to migrate, but the few that do require careful thought. Automating the routine 95% lets the human focus on the 5% that needs them.
If you have a JavaScript codebase that you have been meaning to migrate but have been putting off because the project feels too large, the answer is probably not to wait for a quieter quarter. The answer is to build a workflow that makes the migration cheap enough to run continuously. The migration completes eventually, without disrupting anything else, and the codebase ends up in a better place than it would have if you had tried to do the migration as a project.
If you have been reading along, the first concrete step is to configure your build to accept both .ts and .js files. Once that is working, every subsequent step gets easier. The migration becomes a series of small commits instead of a giant lift. The compounding effect of small commits is how big migrations actually get done.
Build the foundation. Run the inventory. Start migrating leaves. The rest follows.
FAQ
How long did the migration actually take? Six weeks of calendar time, about an hour per day of my time, plus normal review time from the team for the migration PRs.
What language version did you migrate to? The most recent TypeScript at the time. The skills do not care which TypeScript version. They produce types compatible with the target version.
What about React components? React components migrate well because the types are mostly mechanical. The skills handle JSX correctly and produce typed props and state.
What about node_modules dependencies that lack types? The skills produce ambient declaration files for dependencies without types. Most popular dependencies have types available on DefinitelyTyped.
What is the biggest mistake to avoid? Trying to migrate the most complex parts first. Leaves before roots. Easy before hard. The accumulation of small wins gives you the momentum to tackle the hard parts.
If you found this useful, follow for more posts about practical Claude Code workflows. I write about how I run a multi-product business with AI agents handling most of the operational work.
Top comments (0)