Migrating a JavaScript project to TypeScript
I wrote a post back in 2022 about why TypeScript is worth it. This is the companion piece: how to actually migrate an existing JavaScript project without losing your mind.
The key insight is that you do not need to convert everything at once. TypeScript is designed for incremental adoption. You can have .ts and .js files coexist in the same project indefinitely.
Step 1: Add TypeScript to the project
pnpm add -D typescript @types/node
npx tsc --initEdit tsconfig.json to enable gradual migration:
{
"compilerOptions": {
"target": "ES2022",
"module": "Node16",
"moduleResolution": "Node16",
"strict": false,
"allowJs": true,
"checkJs": false,
"outDir": "./dist",
"esModuleInterop": true,
"skipLibCheck": true
},
"include": ["src/**/*"]
}The critical settings: allowJs: true lets TypeScript process JavaScript files. strict: false avoids a flood of errors on day one. checkJs: false means JavaScript files are not type-checked yet.
Step 2: Rename files one at a time
Start with a leaf file (one that does not import many other files). Rename it from .js to .ts. Fix any type errors that appear. Move to the next file.
# Start with utility files, then work up to more complex ones
mv src/utils/format-date.js src/utils/format-date.ts
mv src/utils/calculate.js src/utils/calculate.tsAdd types to function parameters and return values:
// Before (JavaScript)
function formatDate(date) {
return new Intl.DateTimeFormat("en-US").format(date);
}
// After (TypeScript)
function formatDate(date: Date): string {
return new Intl.DateTimeFormat("en-US").format(date);
}Step 3: Add types for external dependencies
Most popular npm packages have type definitions:
pnpm add -D @types/express @types/lodashFor packages without types, create a declaration file:
// src/types/untyped-package.d.ts
declare module "untyped-package" {
export function doSomething(input: string): void;
}Step 4: Enable strict mode gradually
Once most files are converted, start enabling strict checks one at a time in tsconfig.json:
{
"compilerOptions": {
"strict": false,
"noImplicitAny": true,
"strictNullChecks": true
}
}noImplicitAny forces you to type function parameters (the highest-value check). strictNullChecks forces you to handle null and undefined (catches the most runtime errors). Enable these first, fix the errors, then turn on full strict mode.
Step 5: Remove allowJs
Once every file is a .ts or .tsx file, set allowJs: false. The migration is complete.
Tips from doing this multiple times
Do not convert everything in one PR. Convert 5-10 files at a time. Each PR should be reviewable and should not introduce regressions.
Start with utility files and data types. These have the clearest type signatures and fewest dependencies. Work outward from there.
Create shared type definitions early. If your app has a User, Post, or Product type, define it in a types.ts file early and import it everywhere. This establishes the type vocabulary for the project.
Use any as a temporary escape hatch. If a type is complex and blocking your progress, use any and add a // TODO: type properly comment. Come back to it later. The goal is forward progress, not perfection on day one.
Run the build in CI. Add tsc --noEmit to your CI pipeline as soon as you start the migration. This prevents new JavaScript files from being added and catches type errors before merge.
Sources
Related posts
Why I built Omnibase: a universal database MCP server
I got tired of copy-pasting query results between DataGrip and AI agents. So I built an MCP server that gives AI agents secure, direct access to any database.
Delta libraries: how diffing works and which library to use
What delta libraries do, how diff algorithms work under the hood, and a practical comparison of the most popular options in the JavaScript ecosystem.
Offline-first apps: harder than it sounds
Building apps that work without internet is one of those things that seems straightforward until you actually try it. Here is what makes it hard and how to approach it.
Enjoying the blog? Subscribe via RSS to get new posts in your reader.
Subscribe via RSS