TypeScript Migration Guide
Moving a JavaScript project to TypeScript makes the code easier to maintain and gives you a much better editing experience.
This guide walks you through the migration step by step.
Preparation Phase
Assess your codebase. Before you start the migration:
- Work out the size and complexity of the codebase
- Document the build process and the dependencies
- Look for existing type definitions (
.d.tsfiles) - Find the critical paths that need extra care
Set up version control. Make sure you have a clean git repository (or equivalent). Create a branch for the migration and commit the current state, so you can always compare or roll back:
git checkout -b typescript-migrationgit add .git commit -m "Pre-TypeScript migration state"
Configuration
Install TypeScript as a development dependency, together with the Node.js type definitions: npm install --save-dev typescript @types/node
Create tsconfig.json. Start with a basic configuration in the project root:
"compilerOptions": {"target": "ES2020","module": "commonjs","strict": true,"esModuleInterop": true,"skipLibCheck": true,"forceConsistentCasingInFileNames": true,"outDir": "./dist","rootDir": "./src"},"include": ["src/**/*"],"exclude": ["node_modules"]
Note: adjust target to the oldest environment you need to support.
Migration Approaches
There are two ways to migrate.
Gradual migration. Convert one file at a time while the rest stays JavaScript. Enable "allowJs": true and "checkJs": true in compilerOptions so the compiler accepts and checks .js files next to .ts files. Best for large codebases where you want minimal disruption.
All-at-once migration. Rename every .js file to .ts and fix the errors, for example with find src -name "*.js" -exec sh -c 'mv "$0" "${0%.js}.ts"' {} \;. Best for small to medium projects and new (greenfield) projects.
Note: for large projects the gradual approach is strongly recommended — it keeps disruption low and the work manageable.
Step-by-Step Migration
1. Start with configuration. Create a tsconfig.json with these recommended settings, which let TypeScript check your JavaScript without emitting files yet:
"compilerOptions": {"target": "ES2020","module": "commonjs","strict": true,"esModuleInterop": true,"skipLibCheck": true,"forceConsistentCasingInFileNames": true,"outDir": "./dist","rootDir": "./src","allowJs": true,"checkJs": true,"noEmit": true},"include": ["src/**/*"],"exclude": ["node_modules", "dist"]
2. Enable type checking for JavaScript. Add // @ts-check to the top of a JavaScript file and TypeScript starts checking it (see the first snippet). You can silence a single line with // @ts-ignore.
3. Rename files to .ts. Start with non-critical files. Rename one file with mv src/utils/helpers.js src/utils/helpers.ts, or a whole directory (with care) with find src/utils -name "*.js" -exec sh -c 'mv "$0" "${0%.js}.ts"' {} \;.
4. Add type annotations. Give parameters, return values and objects types bit by bit (see the second snippet).
5. Update build and test scripts. Add TypeScript compilation to package.json: "scripts": { "build": "tsc", "dev": "tsc --watch", "test": "jest" }. Make sure your test setup can run TypeScript files too.
Type checking a JavaScript file
With // @ts-check and a JSDoc type, assigning a number to name is reported as an error.
// @ts-check
/** @type {string} */
const name = 'John';
// TypeScript will catch this error
name = 42; // Error: Type '42' is not assignable to type 'string'
Adding type annotations
The same add function before and after typing, plus an interface used as a return type.
// Before
function add(a, b) {
return a + b;
}
// After
function add(a: number, b: number): number {
return a + b;
}
// With interface
interface User {
id: number;
name: string;
email?: string;
}
function getUser(id: number): User {
return { id, name: 'John Doe' };
}
Migration Tools
Some tools can do part of the work for you:
- ts-migrate — an automated JavaScript-to-TypeScript migration tool:
npx ts-migrate-full .(GitHub repository) - TypeStat — converts JavaScript to TypeScript with type safety:
npx typestat(GitHub repository) - @types packages — type definitions for your dependencies, for example
npm install --save-dev @types/react @types/node(search them on TypeSearch)
Best Practices for TypeScript Migration
Start small and iterate
- Begin with utility functions and non-UI components
- Migrate one file or module at a time
- Commit after each successful step
Use TypeScript's features — let inference do the work where it can, use union types for flexibility and type guards for runtime checks (see the snippet).
Handle third-party libraries
- Install
@typespackages for your dependencies - Write declaration files for libraries that have no types
- Use
declare modulefor global type extensions
Useful TypeScript features
Type inference, a union type alias and a type guard function — three features that make migrated code safer with little effort.
// Use type inference where possible
const name = 'John'; // TypeScript infers 'string'
const age = 30; // TypeScript infers 'number'
// Use union types for flexibility
type Status = 'active' | 'inactive' | 'pending';
// Use type guards for runtime checks
function isString(value: any): value is string {
return typeof value === 'string';
}
Common Challenges and Solutions
Dynamic properties
Problem: JavaScript often uses plain objects as dictionaries, adding properties later. TypeScript reports that the property does not exist.
Solution: use an index signature or a type assertion.
Handling this context
Problem: this is lost inside a regular callback function.
Solution: use an arrow function, or bind this explicitly.
Dynamic properties: the problem
Adding name to an empty object literal is an error because its type has no such property.
// Before
const user = {};
user.name = 'John'; // Error: Property 'name' does not exist
Dynamic properties: the solution
Either an interface with an index signature or a type assertion lets the property be added.
// Option 1: Index signature
interface User {
[key: string]: any;
}
const user: User = {};
user.name = 'John'; // OK
// Option 2: Type assertion
const user = {} as { name: string };
user.name = 'John'; // OK
this context: the problem
Inside a regular function callback passed to setTimeout, this is no longer the Counter instance.
class Counter {
count = 0;
increment() {
setTimeout(function() {
this.count++; // Error: 'this' is not defined
}, 1000);
}
}
this context: the solution
An arrow function keeps the surrounding this; alternatively, type this as Counter and call .bind(this).
// Solution 1: Arrow function
setTimeout(() => {
this.count++; // 'this' is lexically scoped
}, 1000);
// Solution 2: Bind 'this'
setTimeout(function(this: Counter) {
this.count++;
}.bind(this), 1000);
Conclusion
Migrating from JavaScript to TypeScript is a big but rewarding investment in your codebase. With this guide you can make the change smoothly and incrementally.
Key takeaways:
- Start with a solid
tsconfig.json - Use
allowJsandcheckJsfor a gradual migration - Let the type system catch errors early
- Update your build and test process to support TypeScript
- Solve the common challenges with the patterns shown above
Migration is a process, not an event. A mixed codebase during the transition is fine — what matters is steady progress while keeping code quality up.
Ready to start? Set up TypeScript in your project and add type annotations gradually; the compiler will guide you toward more robust code. For more information, see the official TypeScript migration guide.