Skip to main content

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.ts files)
  • 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-migration
  • git 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.

script.ts
// @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.

script.ts
// 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 @types packages for your dependencies
  • Write declaration files for libraries that have no types
  • Use declare module for 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.

script.ts
// 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.

script.ts
// 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.

script.ts
// 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.

script.ts
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).

script.ts
// 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 allowJs and checkJs for 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.