Studio · Est. 2009 · Web + Software + Frameworks RSS · Start a Project →
Frameworks
Article · Frameworks

TypeScript Migration Patterns That Actually Worked

Date · 5 January, 2026
Cat · Frameworks
Read · 3 min

Migrating a JavaScript codebase to TypeScript is one of those projects that looks straightforward in a planning document and turns into a quarter of someone's life in practice. We've done it on three codebases of varying sizes in the last eighteen months. Here's what survived contact with production and what we abandoned.

Pick your goal first

"Migrate to TypeScript" is not a goal. It's a path. Before starting, decide what you're actually buying:

  • Fewer runtime bugs from null and undefined. Requires strictNullChecks to be on, eventually.
  • Better refactoring confidence. Requires enough type coverage to make rename-refactors safe.
  • Better editor experience for the team. A lower bar — you get this from allowJs + JSDoc almost for free.
  • Public API contracts you can publish. Requires careful type design at the boundary.

These goals require very different amounts of work. The team that mistakes "better tooling experience" for "strict type safety" will be unhappy halfway through.

The migration patterns we used

JSDoc-first

Add a tsconfig.json with allowJs: true and checkJs: true. Don't rename a single file. Add JSDoc type annotations to existing JS files. The editor lights up, errors get caught, and the team gets a feel for the language without committing.

This is the right first step on any codebase larger than a few thousand lines. It buys 70% of the type-safety value at 10% of the conversion cost. Skip the temptation to rename files until the JSDoc layer is mature.

Module-by-module rename

Once JSDoc is in place, rename leaf modules — utilities, pure functions, small components — first. The dependency graph means renaming a leaf is contained. Renaming a hub means you'll be touching every consumer.

The "barrier" file pattern

For modules that have to keep working with un-converted callers, write a small barrier file that exports the original API with TypeScript signatures. The implementation moves to a .ts file. The barrier handles the boundary. Callers can be converted at their own pace.

Tighten strictness incrementally

Turn on noImplicitAny first. Live with it for a sprint. Then strictNullChecks. Then the rest of strict. Flipping the whole strict flag on day one produces an unmanageable error list. Flipping it one flag at a time produces a finite list per sprint.

What we stopped doing

"Big bang" rewrites

Tempting on small codebases. Disastrous on anything real. Even on 30k-line projects we saw productive teams burn six weeks on rewrites that produced no business value and introduced regressions.

Aggressive use of any

Starting with any everywhere just to get the codebase to compile feels productive and is, in fact, the worst of both worlds. You pay the TypeScript tax (build step, tooling, mental overhead) and get none of the safety. If a type is genuinely unknown, use unknown and narrow at the use site. any is for two specific cases: shimming a third-party library with bad types, and emergency hotfixes you intend to revisit within a week.

Class hierarchies imported from Java

TypeScript will let you build deep class hierarchies. Don't. The idiomatic TS code is closer to "Java verbosity with TS's structural type system" — interfaces, discriminated unions, plain functions. You'll spend less time arguing about generic variance and more time shipping.

The tooling that paid off

  • ts-migrate for initial rename + auto-annotation. Imperfect but saves days.
  • type-coverage in CI. Number-go-up of typed code is a metric the team can rally around.
  • knip and similar dead-code analysis. Migration is a great time to find the modules nobody uses anymore.
  • A pre-commit hook running tsc --noEmit. Stops type regressions from landing.

What it cost

On a 60k-line Node API: about a quarter of senior-engineer time, spread over two quarters, plus elevated PR review burden during the transition. We caught roughly a bug a week that the type system surfaced before runtime. The bugs caught included two that would have shipped to production undetected. We consider that a positive ROI even ignoring the long-term maintenance benefit.

The honest take

TypeScript is worth migrating to on any codebase you expect to live three more years. It is not worth migrating something you'll deprecate in twelve months. The migration is a project, not a checkbox, and the team needs to be told that upfront. Done well, it's one of the highest-leverage investments you can make in a JavaScript codebase's long-term health.