Right, let’s get our hands dirty. We’re not doing a big-bang rewrite where you rename every single file at once and pray for the best. That’s a fantastic way to ruin your entire weekend. Instead, we’re going to do this surgically, one module at a time. This is the “eat the elephant one bite at a time” strategy, except the elephant is your technical debt and we have a very precise fork.

The goal for this phase is simple: change the file extension from .js to .ts (or .jsx to .tsx for React components) on a single, self-contained module and then fix the TypeScript compiler’s tantrums until it shuts up. You’ll do this module by module, gradually building a “green zone” of typed code that starts to fight back against the untyped chaos.

The Order of Battle: Which File to Convert First?

Don’t just pick a file at random. You’re not choosing a movie on Netflix. Strategy here saves you hours of pain.

  1. Start at the Leaves: Begin with modules that have the fewest dependencies. These are the utility libraries, the helper functions, the little formatDate.js file that gets imported everywhere but itself imports almost nothing. Converting these first is easy. You get a quick win, and you start building a foundation of typed code that other modules can lean on.
  2. Avoid the Entangled Core: Do NOT start with app.js or index.js or the central router file. These are the spiderwebs that connect everything. They’ll have a million imports, and converting them first means you’ll have to immediately type the entire universe. It’s overwhelming and will make you want to quit.
  3. Follow the Data: Sometimes it’s easier to start with the modules that define your core data structures. Got a user.js that’s mostly a bunch of object literals? Rename it to user.ts and define your interfaces or types right there. Now, any file that imports a User will get a free, gentle nudge about its shape.

Here’s our perfect first victim, a simple utility module:

// utils.js (The Before Times)
export function capitalize(str) {
  return str.charAt(0).toUpperCase() + str.slice(1);
}

export function getFullName(user) {
  return `${user.firstName} ${user.lastName}`;
}

We rename it to utils.ts. The compiler, tsc, immediately loses its mind. Let’s see why.

The Initial Bloodbath: Letting the Compiler Yell at You

After the rename, run tsc (or however your build process is set up). The error output will be gloriously, wonderfully verbose. Don’t panic. This is the to-do list you didn’t have to write yourself.

For our utils.ts file, the first error will likely be: Parameter 'str' implicitly has an 'any' type. Parameter 'user' implicitly has an 'any' type.

Ah, the classic implicit 'any'. TypeScript’s way of saying, “I have no idea what this thing is, and I refuse to guess.” This is where we start adding types. We’re not being fancy yet, just getting things to compile.

// utils.ts (First Iteration)
export function capitalize(str: string): string {
  return str.charAt(0).toUpperCase() + str.slice(1);
}

export function getFullName(user: any): string {
  return `${user.firstName} ${user.lastName}`;
}

Boom. The first function is now perfectly typed. The second one… well, we used any to shut the compiler up. This is a valid, if cowardly, first step. The goal right now is progress, not perfection. We’ve moved from a completely untyped state to a partially typed state. We can come back and properly type that user object later, once we know what a User actually is (probably from converting that user.js module we talked about earlier).

The Domino Effect: Handling Import/Export Shenanigans

Here’s where it gets interesting. Let’s say you convert that utils.ts file and now you move on to a React component that uses it.

// Welcome.js (before)
import { getFullName } from './utils';

function Welcome({ user }) {
  return <h1>Hello, {getFullName(user)}!</h1>;
}

You rename Welcome.js to Welcome.tsx. Run tsc again. You’ll get a new error, but it’s a good one! Parameter 'user' implicitly has an 'any' type.

Notice what you didn’t get? An error about importing getFullName. Why? Because TypeScript is now reading the types from our already-converted utils.ts! It knows that getFullName expects any and returns a string. We’ve created a tiny, typed contract. As we refine utils.ts to use a proper User interface, the error in Welcome.tsx will automatically become more specific and helpful. This is the virtuous cycle you’re building.

The allowJs Lifeline

This is your most important tsconfig.json setting during a migration. Set "allowJs": true. This magical option lets TypeScript and JavaScript files coexist peacefully in the same project. It means you can have utils.ts importing from legacy-thing.js and vice versa without the world exploding. The compiler will just shrug and say, “Okay, I’ll do my best with this untyped JS file,” while still rigorously checking all your .ts files. You can keep this on for the entire migration period, turning it off only once every last .js file is gone. It’s your safety net.