Home / Blog / TypeScript /Migrating JavaScript to TypeScript: A Guide for Large Projects

TypeScript

October 16, 2026 - by Devico Team

How to Migrate from JavaScript to TypeScript: A Step-by-Step Guide for Large Projects

Creating a tsconfig.json takes minutes. Migrating a 200,000-line JavaScript codebase while several teams continue shipping features can take months.

Syntax is rarely the problem. Migrations lose momentum when teams adopt different conventions, converted files remain full of any, and nobody can show whether the work has improved reliability. A workable migration therefore needs more than a conversion plan. It needs a sequence based on the dependency graph, an enforceable typing standard, and a clear definition of "done."

The conditions for making the move are favorable. TypeScript became the most-used language on GitHub in the 2025 Octoverse report, which has strengthened its tooling, library support, and hiring market. In August 2026, TypeScript 7.0 arrived with a compiler rewritten in Go. Microsoft measured full builds at 8–12 times the speed of the previous compiler — a meaningful change for repositories where type-checking had become part of the migration bottleneck.

Why large codebases move to TypeScript

Most migration proposals begin with one of three recurring problems.

The first is a stream of runtime errors that could have been caught earlier: a missing field, an unexpected undefined, inconsistent representations of the same value, or an API property renamed without updating every caller.

The second appears during onboarding. In an untyped codebase, a new engineer may need to inspect several call sites to understand what a function accepts or returns. Types move much of that information into the editor, where it is available before the engineer changes anything.

Then there is refactoring. Renaming a widely used function or changing a shared data model is risky when the team cannot identify every affected path. TypeScript does not make refactoring automatically safe, but it makes many dependencies visible enough to change with confidence.

Airbnb's migration provides a useful, if company-specific, reference point. Its engineers estimated that 38% of bugs in the codebase could have been prevented with TypeScript. That figure should not be treated as a universal benchmark. It does show that, in a sufficiently large application, static types can catch a material category of defects before deployment.

A full migration is not justified for every project. A short-lived prototype, marketing microsite, or service scheduled for retirement may never recover the cost. The same applies when no one will own the code after the initial conversion. An unfinished TypeScript migration, scattered with ignored errors and inconsistent types, can be harder to maintain than a coherent JavaScript project.

For a small, stable application, JSDoc annotations combined with checkJs may provide enough protection. A full migration makes more sense for code that is large, long-lived, actively changing, and maintained by several engineers or teams.

Incremental or all-in: choosing a migration strategy

Large JavaScript-to-TypeScript migrations generally take one of two forms.

An incremental migration enables allowJs, lets JavaScript and TypeScript coexist, and converts the codebase in manageable sections. Feature development continues throughout the process. Pull requests remain small, releases are easier to protect, and the migration can slow down if product work becomes more urgent.

Time is the trade-off. A large repository may remain bilingual for a year or longer. Without central ownership and CI enforcement, teams can settle on different conventions while any, duplicate types, and partially migrated modules accumulate.

An all-in migration compresses the initial conversion into a dedicated window. Files are renamed in bulk, codemods handle mechanical changes, and a permissive configuration allows the project to compile quickly.

That speed can be misleading. Where the tooling cannot infer a reliable type, it commonly inserts any, @ts-ignore, or @ts-nocheck. The code may compile, but much of it remains effectively untyped. Instead of avoiding the typing work, the team has moved it into a cleanup backlog.

Airbnb followed this concentrated model with its ts-migrate tooling. The company converted projects containing more than 50,000 lines and over 1,000 files in a single day, eventually moving roughly 86% of a six-million-line frontend to TypeScript. The rapid conversion was only the first phase; engineers still had to replace generated stubs with accurate types.

A bottom-up incremental migration takes the opposite approach. Heap, for instance, began with small utility and data modules that had few or no internal dependencies. Types then propagated upward as engineers converted the modules that depended on them. It lacked the drama of a one-day cutover, but each completed file was properly typed rather than added to a separate remediation queue.

Criterion
Incremental (allowJs)
All-in with codemods
AI-assisted

Best fit

Any size; usually the practical choice above roughly 200,000 LOC across teams

Small or mid-sized repositories owned by one team

Mid-sized and large repositories that can split work safely

Production risk

Lowest: changes are small and reviewable

Higher: one large cutover with a permissive baseline

Medium: generated types require close review

Time to compile

Months or longer

Days for the initial conversion

Fast when independent modules can be processed in parallel

Resourcing

Distributed across normal development

Dedicated conversion window followed by cleanup

Engineering time shifts toward instructions, validation, and review

Main debt risk

Fragmented conventions and a prolonged mixed-language state

Large volumes of any and ignored errors

Plausible but incorrect types that still compile

Codebase size matters, but it is not the only consideration. Team structure, delivery commitments, test coverage, and engineering culture can be more decisive. A 40,000-line application owned by one team may tolerate an all-in conversion. A 500,000-line repository shared by six teams usually cannot pause every roadmap for a coordinated cutover.

Voluntary adoption also tends to fragment. If teams can migrate whenever and however they choose, someone still needs to own the shared configuration, type conventions, and quality gates.

Step 1: Map the codebase before estimating the work

Start with an inventory, not a deadline.

Use cloc or an equivalent tool to measure the code by directory and language. Run npm outdated to identify old dependencies that may complicate the migration. Then generate a module dependency graph with a tool such as madge.

The graph is more useful than the line count because it reveals the order in which the code should move. Foundational utilities and data models that depend on little internal code should be converted early. Once those modules expose reliable types, the services, components, and feature modules above them can inherit that information instead of falling back to any.

Third-party dependencies need their own review. Sort them into three groups:

  • Packages that include maintained .d.ts declarations.

  • Packages supported through the @types namespace on DefinitelyTyped.

  • Packages with no usable definitions.

The third category creates the most manual work. Depending on how widely the library is used, the team may need to write declarations, place it behind a typed wrapper, replace it, or deliberately isolate its untyped boundary.

Do not turn the line count into a precise completion date. Tens of thousands of lines may take weeks; hundreds of thousands of lines distributed across several teams usually require a multi-month program. Dynamic behavior matters more than raw size. A smaller application that mutates object shapes at runtime can be harder to type than a much larger codebase built around predictable data models.

Estimates should therefore be ranges, revised after a representative pilot. Convert a few different areas — utilities, API integration code, business logic, and UI components — and use the results to understand where the actual complexity sits.

Step 2: Establish a permissive but controlled baseline

An incremental migration begins by installing TypeScript as a development dependency and configuring allowJs: true. Existing .js files and new .ts files can then remain in the same project while the team converts the repository in stages.

Teams that are not ready to rename files can enable checkJs and add JSDoc annotations first. This exposes type errors and improves editor assistance without changing file extensions. It also helps identify awkward modules before they enter the formal migration queue.

Introduce strictness gradually

Enabling strict: true against a large JavaScript codebase can produce thousands of errors at once. Once the signal is buried under that volume, engineers either disable the checks or start suppressing errors indiscriminately.

Treat strictness as a sequence of smaller changes. Enable an individual flag, resolve or deliberately baseline the resulting errors, and only then add another. noImplicitAny is an obvious target because it exposes gaps in the type model, although it may need to be introduced by package or directory if the initial error count is too large.

The goal is not to keep the configuration permissive indefinitely. It is to make each increase in strictness small enough that the team can understand and complete the work.

Connect TypeScript to the existing build

The implementation depends on the stack. Vite and esbuild can strip types during builds with little configuration. Webpack projects commonly use ts-loader or a Babel preset.

Node.js has reduced the need for a separate compilation step in some backend applications. TypeScript execution through native type stripping is enabled by default in Node v22.18 and v23.6 and became unflagged as stable in v24.12 and v25.2.

Type stripping erases annotations; it does not transform TypeScript syntax that produces runtime JavaScript. Features such as enums, parameter properties, and namespaces containing runtime code are therefore unsupported in this mode. .tsx is not handled either. TypeScript 5.8 and later provide erasableSyntaxOnly for teams that want the compiler to enforce a syntax subset compatible with this execution model. Projects that rely on richer TypeScript features still need a compiler or bundler.

Step 3: Convert files in dependency order

Once the baseline is stable, the migration becomes a repeatable engineering task:

  1. Select a foundational module or tightly connected group of files.

  2. Rename .js to .ts and .jsx to .tsx.

  3. Replace implicit contracts with explicit types.

  4. Run the type checker and relevant tests.

  5. Review, merge, and move one level higher in the dependency graph.

Starting with a top-level feature may look more visible, but it often creates rework. If its utility functions, API clients, and models are still untyped, the feature will either inherit any or create local definitions that later conflict with the shared versions.

Define the central domain types early: API request and response shapes, persistent entities, shared state, and the objects passed across module boundaries. These definitions should have an obvious home. Otherwise, separate teams may each create a slightly different User, Order, or Account, leaving the code technically typed but conceptually inconsistent.

React code deserves a separate migration track. Props, state, hooks, refs, children, and DOM events introduce a different set of decisions from ordinary business logic. Engineers who are comfortable typing utilities may still need guidance with component generics or event handlers.

Keep conversion pull requests narrow. One module — or a small cluster that cannot be separated — is easier to review than a broad rename touching several unrelated features. Small changes also make rollback practical if a production issue appears.

Tests determine how quickly a file can move. Well-covered modules can be renamed, typed, and verified with reasonable confidence. Risky logic without coverage needs a characterization test before conversion or stronger manual review. TypeScript catches contract errors; it does not prove that the business behavior remains correct.

Step 4: Automate the mechanical work, then review the meaning

Renaming thousands of files by hand is not a good use of engineering time. Codemods can update imports, rename extensions, convert some JSDoc annotations, and insert temporary suppressions where inference fails.

Airbnb's ts-migrate remains the best-known tool for this work. Its development has largely stalled, so it is better treated as a stable utility than an actively evolving migration platform. Test it against the TypeScript version and coding patterns used in your repository before applying it broadly.

Codemods solve a mechanical problem. They cannot reliably decide what a domain object is supposed to contain or whether a function should accept two related shapes. When uncertain, they normally choose a permissive result that allows compilation.

Every generated any, @ts-ignore, and @ts-nocheck should therefore enter a visible cleanup queue. If these suppressions disappear into the migrated code, the team receives the maintenance cost of TypeScript without much of its protection.

AI-assisted tooling can handle more of the semantic work. Coding agents can inspect usage across files, propose interfaces, convert related modules, and fix compiler errors. Independent areas can be processed in parallel through isolated Git worktrees.

The results depend heavily on the instructions. Agents need to know where shared types belong, which syntax is allowed, how strict each package should be, and whether existing patterns should be preserved or corrected. Without those constraints, several agents may produce individually reasonable but mutually inconsistent definitions.

That is why teams evaluating TypeScript development companies should ask about their experience with codemods, AI-assisted workflows, and post-conversion validation — not simply the number of engineers they can assign.

Automation can get a repository to "compiles." It cannot certify that the types describe the software correctly. Human review and a scheduled cleanup phase remain part of the migration.

Step 5: Put the migration rules into CI

A long migration is vulnerable to backsliding. While one group converts existing modules, another may add new JavaScript files, introduce fresh any types, or bypass errors with suppressions. Written guidance alone will not stop this.

Add tsc --noEmit as a required CI check. If the chosen baseline already compiles cleanly, every pull request must preserve that state. If the repository begins with known errors, record the baseline and configure the migration so the count can decrease but never increase.

This ratchet matters more than the speed of the initial conversion. It ensures that feature development cannot expand the untyped surface faster than the migration reduces it.

Compilation alone is an incomplete measurement. A project can pass tsc while a large share of its expressions remain typed as any. Track type coverage in CI so the team can see how much code has a meaningful, non-any type. The trend should move in one direction.

Shared definitions also need enforcement through architecture and review. A central types package or clearly owned directory reduces the chance that separate modules will maintain incompatible versions of the same concept. The CI ratchet prevents new errors; centralized types prevent a different kind of fragmentation that the compiler may not recognize.

Keep the migration from exhausting the team

A migration is not spare-time maintenance. Engineers must learn new patterns, review unfamiliar type errors, and continue delivering roadmap work. If management plans only for the file conversion, the workload will surface elsewhere as slower features or weaker reviews.

Pair engineers who are new to TypeScript with someone fluent during their first conversions. Code review should explain why a type is structured a certain way, not merely reject the implementation. This spreads the knowledge needed to maintain the result once the migration team disbands.

Rotate people through the work. Six months of renaming and repairing old modules is an efficient way to make a capable engineer resent the project. A rotation keeps feature context fresh, distributes TypeScript knowledge, and avoids turning migration work into a permanent assignment for a few people.

Leadership also needs evidence that the program is progressing. Agree on the intended outcomes before kickoff: fewer production errors caused by invalid data shapes, safer refactoring, or shorter onboarding time. Report type coverage alongside those measures.

The first few weeks may be slower. Engineers are learning, configuration is changing, and the most reusable types are still being established. The benefits tend to become clearer later, once editor feedback improves and the compiler begins catching mistakes across already-typed boundaries.

Define what a successful migration looks like

A migration without a finish line tends to become a permanent backlog.

Type coverage is the clearest implementation metric because it distinguishes meaningful types from code that merely compiles. Set the target before the work expands. A threshold around 95% may be appropriate for some repositories, provided strict rules apply to new code and the remaining exceptions are documented and owned.

Production error data provides the strongest business evidence when a reliable baseline exists. Compare incidents involving null values, invalid property access, and mismatched data shapes before and after the migration. Internal before-and-after data is more useful than a generic industry claim because it reflects the application the team actually maintains.

Build performance and editor responsiveness also matter. A migration that makes every pull request significantly slower will create pressure to bypass the checks. TypeScript 7's native Go compiler, measured at 8–12x faster than the previous tsc on full builds, should reduce that trade-off for large repositories.

Metric
How to measure it
Practical completion target

Type coverage

Run a type-coverage tool in CI and track the percentage of non-any expressions

An agreed threshold, such as approximately 95%, with a documented plan for exceptions

Production errors

Compare incident and error-tracking data before and after migration

A sustained decline in null-reference and data-shape failures

Strictness

Monitor enabled compiler flags and require tsc --noEmit to pass

Strict rules applied to all new code

Build and CI time

Track type-check duration in the pipeline

Stable or improving execution time

New-code gate

Make the type check a required pull-request status

No merge introduces a new type error

Suppression debt

Count any, @ts-ignore, and @ts-nocheck usage

A declining count with named owners for remaining exceptions

"Done" does not have to mean 100% strict coverage on the first pass. Some legacy integrations or highly dynamic modules may be disproportionately expensive to convert. The important distinction is whether those gaps are deliberate and contained or simply forgotten.

When external TypeScript expertise makes sense

Outside support becomes useful when the migration competes directly with product delivery, lacks an experienced technical owner, or must meet a deadline the internal team cannot support.

One option is a dedicated migration track operating alongside the existing product teams. The external engineers handle inventory, conversion, CI configuration, and cleanup while the core team continues feature development. Another is a shorter architecture engagement that defines the migration order, compiler policy, type conventions, and automation approach before internal engineers take over.

Organizations may also hire experienced TypeScript developers to close a specific capability gap rather than outsource the entire program.

Selection should focus on how the partner handles the difficult parts:

  • How do they establish a trustworthy baseline?

  • How do they measure and reduce any usage?

  • What is their process for reviewing codemod or AI-generated types?

  • How do they prevent separate teams from defining conflicting domain models?

  • How will internal engineers learn to maintain and extend the migrated code?

A migration that produces typed code but leaves the internal team dependent on the vendor has not resolved the underlying problem.

The main decision is still internal: choose a migration shape that fits the repository and team structure, make regression impossible through CI, and define the completion threshold before conversion begins. With those controls in place, moving from JavaScript to TypeScript becomes a measured engineering program rather than a prolonged rename exercise.

Frequently asked questions

How long does it take to migrate a large JavaScript codebase to TypeScript?

There is no dependable fixed rate. Tens of thousands of lines may take several weeks, while hundreds of thousands of lines across multiple teams usually require months. The largest variable is not line count but the amount of dynamic behavior, inconsistent data, and unsupported third-party code the repository contains.

A pilot covering several representative modules will produce a more useful estimate than a generic lines-per-week benchmark.

Can you migrate to TypeScript without pausing feature development?

Yes. Enable allowJs so JavaScript and TypeScript files can coexist, then migrate incrementally while normal development continues.

The approach only works if CI prevents new type errors and the team has rules for newly created files. Otherwise, feature work can add untyped code as quickly as the migration removes it.

Is ts-migrate still the best automated migration tool in 2026?

ts-migrate remains useful for bulk renaming, import changes, and temporary stubbing, although its own development has largely stalled. Teams increasingly supplement it with AI-assisted coding tools that can inspect usage and propose more detailed types.

Neither approach eliminates human review. Both can generate code that compiles while preserving incorrect assumptions or excessive any usage.

Does the project need 100% strict mode before the migration is complete?

Not necessarily. A practical finish line may be a high type-coverage threshold, strict enforcement for all new code, and a documented plan for the few remaining modules.

Pursuing 100% strictness immediately can consume substantial effort for little additional protection, especially around legacy integrations. Those exceptions should be explicit rather than allowed to spread.

Should a codebase above 100,000 lines migrate all at once or incrementally?

For a repository above roughly 100,000 lines — particularly one shared by several teams with active roadmaps — an incremental migration is usually safer. It avoids freezing feature development and keeps changes small enough to review and release normally.

An all-in codemod conversion can still work when one team owns the code and can reserve a focused conversion window plus a genuine cleanup phase.

Stay in touch

Leave your email and we will inform you about all our news and updates

 

Up next