33.6 Automating Migration with ts-migrate and TypeScript-Migrate
Right, so you’ve decided to stop living in the wild west of JavaScript and put on the structured, slightly-constricting-but-in-a-good-way suit of TypeScript. Good for you. Doing this by hand for a large codebase is a special kind of masochism I don’t recommend. This is where automation tools like ts-migrate (from Airbnb) and typescript-migrate (a similar concept) come in. They’re not magic wands, but they’re the closest thing we have to a bulldozer that can clear the initial path. Think of them as your over-caffeinated intern who does 80% of the grunt work incredibly fast, but you absolutely must check their work before you ship it.
These tools work on a simple but brutal principle: make it compile, now. Their primary goal is to take your sprawling .js files and transform them into .ts (or .tsx) files that the TypeScript compiler will accept without immediately throwing a tantrum. They do this by slapping any types on everything they can’t immediately figure out. It’s not elegant, but it gets you from zero to a compiling codebase in minutes, not months.
Getting Your Hands on the Tools
First, you need to get the thing. Airbnb’s ts-migrate is a bit more involved to set up because it’s a plugin-based system. You’ll likely clone their repository and use it as a CLI tool on your project.
# Clone the ts-migrate repository
git clone https://github.com/airbnb/ts-migrate.git
cd ts-migrate
# Install its dependencies
yarn install
yarn build
# Then, from your project's root directory, run it on a specific package
npx ../path/to/ts-migrate/ migrate <package-name>
Alternatively, if you’re in a smaller project or want something you can npm install directly, you might find a package like typescript-migrate (note the ’s’).
# This is a different, more standalone package
npm install -g typescript-migrate
# Then, from your project root:
typescript-migrate init
typescript-migrate fix
The exact commands will vary, so always check the latest README. The point is, you’re running a script that will violently reshape your codebase.
What to Expect: A Tsunami of any
Let’s be perfectly clear about what these tools are good for and what they are not good for. They are excellent at:
- Renaming
.js/.jsxfiles to.ts/.tsx. - Adding basic
tsconfig.jsonfiles if you don’t have one. - Inserting massive amounts of
// @ts-expect-errorcomments above lines the compiler immediately complains about. - Inferring and adding the most obvious types. For a function like
function sum(a, b) { return a + b; }, it can correctly infernumber.
But their signature move, their pièce de résistance, is plastering any on every variable, parameter, and return type that isn’t blindingly obvious. Behold, the before and after:
// Before: glorious, untyped JavaScript freedom.
export function processUserData(user, config) {
const id = user.id.toString();
const settings = { ...defaultSettings, ...config };
return transform({ id, user, settings });
}
// After: the ts-migrate special.
export function processUserData(user: any, config: any): any {
const id: any = user.id.toString();
const settings: any = { ...defaultSettings, ...config };
return transform({ id, user, settings });
}
It’s… technically TypeScript. The compiler will shut up and run. But you’ve traded one kind of ignorance for another. This is not the end goal; this is the starting line.
The Real Work Begins After the Migration
This is the most important thing I will tell you: The automated migration is the easy part. Running the tool takes minutes. Cleaning up the any-pocalypse takes days or weeks. The tool gives you a compiling codebase so you can start tightening the screws incrementally without blocking everyone else.
Your new full-time job is to replace those any types with actual types. Here’s your battle plan:
- Enable
noImplicitAnyin yourtsconfig.json. Do this after the migration. This will force the compiler to scream about every leftoverany, turning your codebase into a glorious error festival. This is your to-do list. - Start with the low-hanging fruit. Functions that clearly return
booleanorstring. Simple object shapes. Knock these out quickly to build momentum. - Create and use central interfaces. That
userobject that’s currentlyany? Find where it’s used, figure out its real shape, and define an interface likeUserorIUser(I prefer the former, the TypeScript team thinks theIprefix is PascalCase for “old”). - Tackle one file or one module at a time. Don’t try to boil the ocean. The beauty of this approach is that the code still works while you methodically improve type safety.
The Inevitable Rough Edges and Pitfalls
These tools are clever, but they’re not sentient. They will make questionable choices.
- Third-Party Libraries: They’ll try to add
import * as React from 'react';but might flub less common libraries. You’ll be manually fixing import statements. - The
// @ts-expect-errorOverkill: It will add these suppression comments with reckless abandon. Many of these are easy fixes (e.g., checking fornull). Treat these comments not as permanent fixtures, but as markers for future cleanup. Delete them aggressively as you add proper types. - Complex Inference Failures: Any function with a moderately complex signature or generic behavior will be reduced to
any. The tool just can’t reason about it. This is your domain knowledge to fix.
The automated migration is a means to an end. It’s a tactical nuke that lets you skip the trench warfare of a manual, file-by-file conversion. Embrace the mess it creates, because that mess is now a compiling mess, and you can roll up your sleeves and start turning it into something actually type-safe. Now get to it.