How to Migrate a JavaScript Codebase to TypeScript Incrementally
A practical, step-by-step guide to migrating JavaScript to TypeScript without stopping feature work or drowning in compiler errors. 700+ words.
You have a working JavaScript codebase. Maybe it is 20 files, maybe 200. Either way, you have heard the pitch for TypeScript and decided it is worth doing — but a full rewrite is not realistic. You need to ship features while the migration happens around you.
The good news: TypeScript was designed for exactly this scenario. The bad news: most teams attempt too much at once, get buried in hundreds of compiler errors, and quietly revert their changes two weeks later. Here is how to do it right.
The Core Strategy: Migrate JavaScript to TypeScript File by File
TypeScript's allowJs flag lets you mix .js and .ts files in the same project. That is your escape hatch. You never have to migrate everything at once.
Start with the tsconfig, not the files.
Create a tsconfig.json at the root with these settings:
{
"compilerOptions": {
"allowJs": true,
"checkJs": false,
"strict": false,
"outDir": "dist",
"target": "ES2022",
"module": "NodeNext",
"moduleResolution": "NodeNext"
},
"include": ["src"]
}allowJs lets TypeScript compile your existing .js files. checkJs: false means it will not yell at your old JavaScript yet. strict: false gives you room to migrate without enabling every flag on day one. This configuration should produce zero new errors.
Run tsc --noEmit. If it fails, fix those issues before touching a single file.
Phase 1: Migrate the Leaves First
Your codebase has a dependency graph. Utility functions and pure helpers have no internal imports — they are the leaves. Start there. Rename utils/format.js to utils/format.ts. Add types to the function signatures. Fix the errors TypeScript surfaces. Commit.
Do not touch files that import from many places yet. If you rename db.js to db.ts on day one, you will face cascading errors across every file that imports it.
Work inward from the edges toward the entry points. A good order:
- Pure utility functions
- Type definition files (create a
types/directory with.tsfiles) - Service modules with narrow dependencies
- Route handlers and controllers
- Entry points last
Phase 2: Tighten the Config Gradually
Once 70-80% of your files are .ts, you can start tightening. Flip these flags one at a time and fix the errors each one surfaces:
"strict": true— enablesstrictNullChecks,noImplicitAny, and six others"noUncheckedIndexedAccess": true— catches array/record access that can returnundefined"noImplicitReturns": true— ensures every code path in a function returns a value"checkJs": true— starts checking any remaining.jsfiles
Enable one flag per PR. Review what breaks. Fix it. Merge. Repeat.
Phase 3: Delete the allowJs Escape Hatch
Once every file is .ts, remove allowJs and checkJs. Set strict: true if you have not already. Run tsc --noEmit in CI on every push so regressions are caught before they reach main.
Common Pitfalls That Slow Teams Down
Using as to silence errors. Type assertions like const user = data as User feel like fixes but they are not. They tell TypeScript to trust you, and TypeScript will. If the runtime shape does not match, you get a crash with no warning. Use runtime validation (Zod, Valibot) at trust boundaries instead.
Enabling strict on an unmigrated codebase. If you flip strict: true before renaming any files, you will see thousands of errors in your JavaScript. TypeScript cannot infer types it does not have. Gradual strictness is the point.
Migrating shared packages last. If your project is a monorepo, migrate shared packages — utilities, API clients, shared types — before the applications that consume them. A typed package gives you accurate inference across the board.
Forgetting to type external data. TypeScript types are compile-time constructs. JSON from an API, data from a database, environment variables — these are untyped at runtime regardless of what TypeScript thinks. Add schema validation at every ingestion point.
How Long Does It Take
A 10,000-line codebase with one focused engineer typically reaches full .ts coverage in two to four weeks when done incrementally. The first week is the tsconfig setup, leaf migration, and building the muscle. Weeks two and three are the middle of the graph. The final push is usually the cleanest.
The milestone that matters most is not "all files renamed" — it is tsc --noEmit running green in CI. Everything before that is preparation.
If you are building a TypeScript-first product from scratch or need expert help on a migration that has stalled, talk to Clixo. We build production systems and know where the traps are.