Skip to main content

Migrate JS to TS Introduction

Starting a new project in TypeScript is easy. Converting a legacy, dynamically-typed JavaScript codebase of 50,000 lines to TypeScript is one of the hardest challenges in frontend engineering.

The mental model

You cannot migrate a large project by halting all feature work for a month, renaming every file to .ts, and fixing 10,000 compiler errors. Business requirements won't allow it, and the merge conflicts will destroy the team.

The only successful migration strategy is incremental.

You configure TypeScript to allow JavaScript files to coexist with TypeScript files. You migrate one file at a time, from the "leaves" of the dependency tree up to the "trunk".

The Two Approaches

  1. Strict from Day One (Recommended): You configure tsconfig.json with strict mode enabled ("strict": true). You leave legacy files as .js. When you decide to rename a file to .ts, you must fix all types perfectly in that specific file before committing.
  2. Loose then Tighten: You rename everything to .ts immediately, but you configure TS to be extremely loose ("noImplicitAny": false, "strictNullChecks": false). You fix errors gradually over months, slowly turning on strict flags.

The loose approach is dangerous because it provides a false sense of security — the files have a .ts extension, but are effectively just JavaScript.

The "Bottom-Up" Strategy

Dependencies flow downwards. App.js imports UserList.js, which imports UserCard.js, which imports formatDate.js.

If you migrate App.js first, you will have to type everything blindly because its dependencies are still untyped JS.

Always migrate bottom-up. Start with utility functions (formatDate.js), then pure components (UserCard.js), then stateful containers (UserList.js). By the time you reach App.js, everything it imports is already fully typed.

Successful TypeScript migrations are incremental and bottom-up. Configure the compiler to allow .js and .ts coexistence, and migrate utility functions and leaf components first.