33.1 The Incremental Migration Strategy: allowJs and checkJs
Right, so you’ve decided to stop living in the wild west of JavaScript and put on the structured, slightly-stiff-but-incredibly-warm jacket of TypeScript. Good choice. But you’re not about to stop the entire product roadmap, lock the team in a basement for six months, and rewrite 200,000 lines of code in one go. That’s a fantastic way to get fired, or at the very least, develop a nervous twitch.
We’re going to be smart about this. We’re going to use TypeScript’s built-in escape hatches and incremental flags. This isn’t an all-or-nothing proposition. The two key players in this strategy are allowJs and checkJs. Think of them as the training wheels and the slightly anxious parent running behind the bike, respectively.
The allowJs Lifeline
First, allowJs. This is your get-out-of-jail-free card. By default, the TypeScript compiler (tsc) only looks at .ts and .tsx files. If you throw a .js file at it, it’ll scoff and ignore it. When you set allowJs to true in your tsconfig.json, you’re telling the compiler, “Hey, chill out. We’re in a transition period. Please just accept my JavaScript files and include them in your compilation.”
This is non-negotiable for a gradual migration. It allows you to rename files from .js to .ts one by one, at your own pace, while the entire application continues to build and work. The compiler will simply copy your .js files over to the output directory alongside the compiled .ts files.
Here’s what your tsconfig.json might look like in the early days:
{
"compilerOptions": {
"target": "es2017",
"module": "commonjs",
"outDir": "./dist",
"rootDir": "./src",
"allowJs": true, // <-- The Magic Switch
"checkJs": false, // We'll get to this monster in a second
"strict": false // For now, we're being gentle
},
"include": ["src/**/*"]
}
With this setup, you can start renaming files. utils.js becomes utils.ts. You can start adding types to it slowly. The rest of your app that still imports from ./utils doesn’t care. The import path doesn’t change. It’s a beautiful thing.
Unleashing the Chaos with checkJs
Now, allowJs is a pacifist. It just accepts JavaScript and moves on. checkJs is its hyper-competitive, pedantic sibling. When you set checkJs to true, the TypeScript compiler will actually type-check all your JavaScript files according to its inference rules, as if they were .ts files.
This is where you start to see the terrifying reality of your codebase. It will find all the implicit anys, the questionable truthiness checks, the functions that sometimes return a string and sometimes return undefined for no apparent reason.
Do not turn this on for your entire project all at once. I’m serious. You will be inundated with thousands of errors. The team will revolt. You will be tempted to just // @ts-ignore the entire codebase and call it a day, which defeats the whole purpose.
Instead, use it surgically. On a file-by-file basis, with a JSDoc comment:
// @ts-check
/**
* @param {number} a
* @param {number} b
* @returns {number}
*/
function add(a, b) {
return a + b;
}
const result = add(5, 'nope'); // 🚨 ERROR: Argument of type 'string' is not assignable to type 'number'.
See that? We just got a TypeScript error in a .js file. This is incredibly powerful. It allows you to start adding type safety and finding bugs before you even commit to renaming the file to .ts. You can use JSDoc annotations like @param, @returns, and @type to gradually add type information right there in the JavaScript.
The Order of Operations
Here’s the smart, trench-approved workflow for each file:
- Start with
// @ts-check: Add this to the top of a.jsfile you want to start hardening. Let the errors roll in. Use JSDoc to type the function signatures and important variables. Fix the obvious bugs it finds. This is your testing ground. - Rename to
.ts: Once you’ve cleaned up the most egregious errors with// @ts-checkand JSDoc, rename the file to.ts. Now you’re playing for real. You can use proper TypeScript syntax, interfaces, and generics. The errors might change slightly, but the foundation is laid. - Increase strictness gradually: As you rename more files, you can start enabling stricter options in your
tsconfig.json(noImplicitAny,strictNullChecks, etc.) for the entire project. You’ll get errors in your new.tsfiles, which is what you want, while the remaining.jsfiles are left alone (unless you’ve also enabledcheckJsglobally, which you still shouldn’t).
The Pitfalls (Because Of Course There Are Some)
- Third-Party Libraries: Your JavaScript files might use libraries that don’t have types. You’ll need to install
@types/packages for them or create a quick ambient module declaration (declare module 'that-weird-library';) to shut up the errors until you can properly address it. - The Inferrence is Lax in JS: TypeScript’s type inference in
.jsfiles is intentionally more permissive than in.tsfiles. Don’t expect it to catch everything. Its main job withcheckJsis to validate the explicit hints you give it via JSDoc. - Build Tooling: Ensure your build tool (Webpack, Vite, etc.) is configured to handle a mix of
.jsand.tsfiles. Most modern tools do this out of the box, but it’s worth verifying. You’re not just runningtsc; you’re probably running it as part of a larger pipeline.
This strategy isn’t just the best way to migrate; it’s the only sane way for any non-trivial application. It lets you move at your own pace, prove value immediately by finding bugs in existing JS, and avoid a single massive, risky rewrite. Now go turn allowJs on. I’ll wait.