Migrating a JavaScript codebase to TypeScript can feel like a daunting task, especially for large projects. But with the right approach, it can be done incrementally, safely, and without disrupting your development workflow. In this guide, I'll walk you through the process step by step.
Why Migrate to TypeScript?
TypeScript adds type safety to JavaScript, catching entire categories of bugs at compile time instead of runtime. It provides better tooling with autocompletion, refactoring support, and inline documentation. For large codebases with multiple developers, these benefits are transformative.
The Key: Gradual Migration
The key is to approach the migration as a gradual process, not a big bang rewrite. You can start with a single file and expand from there, getting value from TypeScript incrementally. This minimizes risk and lets your team adapt to TypeScript gradually.
Step 1: Set Up TypeScript
Start by installing TypeScript and creating a configuration file. The most important decision is how strict to make your initial configuration.
Installation
npm install --save-dev typescript @types/node
npx tsc --init
Initial Configuration
For the initial setup, keep the configuration loose. Enable allowJs to let TypeScript process your existing JavaScript files. Set strict to false initially and gradually enable strictness as your codebase becomes more typed.
{
"compilerOptions": {
"target": "ES2020",
"module": "commonjs",
"allowJs": true,
"checkJs": false,
"outDir": "./dist",
"rootDir": "./src",
"strict": false
},
"include": ["src/**/*"]
}
This configuration lets TypeScript process your JavaScript files without requiring any changes. You can verify that the build works before making any type-related changes.
Step 2: Enable Type Checking Gradually
Once TypeScript is set up and building successfully, enable checkJs to start getting type information from your JavaScript files. TypeScript will infer types from your existing code and report any issues it finds.
Dealing with Type Errors
At this point, you will see type errors in your JavaScript files. Do not try to fix them all at once. Instead, use // @ts-nocheck at the top of files you are not ready to type yet, and remove it as you migrate each file.
This lets you migrate at your own pace without breaking the build.
Step 3: Rename Files to .ts
Start migrating files one at a time by renaming them from .js to .ts. Begin with files that have few dependencies and clear, simple types. Utility functions, helper modules, and data transformation functions are good candidates.
Adding Type Annotations
When you rename a file, you will need to add type annotations for function parameters and return types. Start with the most important types and use any as a temporary placeholder for complex types you are not ready to define yet.
// Before: JavaScript
function calculateTotal(items) {
return items.reduce((sum, item) => sum + item.price, 0)
}
// After: TypeScript
function calculateTotal(items: Array<{ price: number }>): number {
return items.reduce((sum, item) => sum + item.price, 0)
}
Step 4: Define Shared Types
As you migrate files, you will notice the same types appearing in multiple places. Extract these into shared type definitions. Start with your API contracts, database models, and core domain types.
Organizing Types
Create a types directory and organize types by domain. This makes them easy to find and reuse across your codebase.
// types/user.ts
export interface User {
id: string
email: string
name: string
createdAt: Date
}
export interface CreateUserRequest {
email: string
name: string
password: string
}
Step 5: Enable Strict Mode
Once most of your codebase is typed, enable strict mode in your TypeScript configuration. This enables several checks that catch more bugs but require more type annotations.
What Strict Mode Catches
Strict mode enables strictNullChecks, which forces you to handle null and undefined explicitly. This will likely reveal many issues in your codebase. Fix them one at a time, using the compiler errors as your guide.
{
"compilerOptions": {
"strict": true
}
}
Step 6: Add Runtime Validation
TypeScript types are erased at runtime, so they cannot protect you from malformed data that arrives over the network or from user input. Add runtime validation using libraries like Zod to ensure that data matches your types at runtime.
import { z } from 'zod'
const UserSchema = z.object({
id: z.string().uuid(),
email: z.string().email(),
name: z.string().min(2).max(100),
createdAt: z.string().datetime()
})
type User = z.infer<typeof UserSchema>
This gives you the best of both worlds: compile-time type safety from TypeScript and runtime validation from Zod.
Common Pitfalls to Avoid
The biggest mistake teams make is trying to migrate too quickly. A big bang migration where you rename all files at once and fix all type errors is risky and disruptive. Take it file by file, and keep your build green throughout the process.
Another common mistake is overusing any. It is tempting to use any as a quick fix for type errors, but it defeats the purpose of using TypeScript. Use any sparingly and plan to replace it with proper types later.
Do not forget to update your tests. TypeScript can catch type errors in your test files too, and migrating your tests helps ensure they stay in sync with your production code.
Frequently Asked Questions
How long does a migration take?
It depends on the size of your codebase. A small project (10-20 files) can be migrated in a few days. A large project (100+ files) might take several weeks or months. The key is to migrate incrementally and keep your build working throughout the process.
Yes, TypeScript integrates with most modern development tools. Most editors have excellent TypeScript support. Build tools like Webpack, Vite, and esbuild all support TypeScript. Testing frameworks like Jest and Vitest work with TypeScript.
What if I have third-party libraries without types?
Most popular libraries include TypeScript definitions. For libraries without types, install @types packages from DefinitelyTyped. If types don't exist, you can write your own declaration files or use any as a temporary workaround.
Should I migrate tests?
Yes, migrate your tests along with your production code. TypeScript catches type errors in test files too, and migrating tests helps ensure they stay in sync with your production code.
What if my team doesn't know TypeScript?
Invest in training. Run lunch-and-learn sessions, pair program, and create internal documentation. The investment in learning TypeScript pays off quickly in improved productivity and fewer bugs.
The Bottom Line
A successful migration balances speed and stability. Start with a loose TypeScript configuration, migrate files one at a time, define shared types as you go, enable strict mode gradually, and add runtime validation. Incremental progress, compiler feedback, and practical validation make the process manageable and safe.
Remember: the goal isn't to achieve 100% type coverage immediately. The goal is to get value from TypeScript incrementally while keeping your codebase stable and your team productive.