Migrating a JavaScript Codebase to TypeScript

Chandrashekhar Fakirpure

Mar 14, 2025

Share:

In this tutorial, we'll learn how to Migrate a JavaScript Codebase to TypeScript.

Introduction

TypeScript is a superset of JavaScript that adds static typing. With TypeScript, you can detect issues before your code even runs, saving time on debugging and ensuring smoother team collaboration. Whether you’re refactoring a small personal project or a large-scale enterprise application, introducing TypeScript will help everyone write cleaner, more robust code.

Transitioning from JavaScript to TypeScript can seem daunting, but the end result—more robust, maintainable, and self-documenting code—is well worth the effort. This walkthrough aims to streamline the process so that you can integrate TypeScript with minimal hassle and maximum payoff.

Why Migrate to TypeScript?

JavaScript’s flexibility is part of its appeal, but it also allows for ambiguous scenarios that lead to bugs at runtime. TypeScript solves this problem by bringing type-checking to your existing JavaScript.

Fewer Bugs at Runtime

Type errors are caught during compilation instead of surfacing as unexpected crashes or behavior in production.

Better Tooling

TypeScript enables richer IntelliSense (autocompletion, refactoring suggestions) in popular IDEs like Visual Studio Code, JetBrains, etc.

More Maintainable Code

Types act like living documentation. A well-typed codebase is simpler to understand and change—even years down the line.

Scalable for Teams

Strong typing is a huge help for large teams, ensuring that one developer’s changes don’t break others’ work.

Migrating a JavaScript Codebase to TypeScript

Step 1: Set Up the Essentials

1.1 Install TypeScript

If you haven’t already, install TypeScript as a development dependency:

npm install --save-dev typescript

Or, if you’re using Yarn:

yarn add --dev typescript

1.2 Create/Initialize tsconfig.json

At the root of your project, run:

npx tsc --init

This automatically generates a tsconfig.json with sensible defaults. Here’s an example configuration to illustrate some recommended options:

{
  "compilerOptions": {
    "target": "ES2022",
    "module": "ESNext",
    "outDir": "dist",
    "strict": true,
    "esModuleInterop": true,
    "skipLibCheck": true,
    "allowJs": true,
    "forceConsistentCasingInFileNames": true
  },
  "include": ["src/**/*"],
  "exclude": ["node_modules", "**/*.test.ts"]
}

One of the first steps when you introduce TypeScript to an existing JavaScript project is creating a tsconfig.json file at the root of your codebase. This file controls how the TypeScript compiler behaves and where it outputs compiled files. Here are some important properties:

compilerOptions: This is where you specify key settings like target (the JavaScript version you want to compile to), module (the module system you want to use, such as CommonJS or ESNext), and outDir (where compiled files go).

Let’s break down a few important options:

allowJs: true

Allows your existing JavaScript files to co-exist and be compiled by the TypeScript compiler, facilitating an incremental migration.

strict: true

Enables all strict type checking options. This gives the best safety but can also flag many issues upfront. If it feels overwhelming, you can disable it temporarily by setting it to false or selectively disable options like strictNullChecks or noImplicitAny.

skipLibCheck: true

Skips type-checking of .d.ts files in your node_modules, speeding up compilation. This often prevents minor type mismatches in external definitions from interrupting your workflow.

forceConsistentCasingInFileNames: true

Prevents subtle bugs by enforcing consistent filename casing, especially important in cross-platform teams.

Step 2: Adjust Your Build Scripts

Inside your package.json, add a script to compile TypeScript:

{
  "scripts": {
    "build": "tsc"
  }
}

When you run npm run build (or yarn build), TypeScript will compile everything specified in your tsconfig.json into your outDir (e.g., dist). You can also add a watch mode for local development:

{
  "scripts": {
    "build": "tsc",
    "watch": "tsc --watch"
  }
}

Step 3: Transitioning Files (The Gradual Approach)

3.1 Rename Files Incrementally

You don’t have to flip your entire project to TypeScript all at once. Instead:

  • Pick a small feature or module—ideally one that’s core to your application or something you need to modify soon.
  • Rename that file from .js to .ts (or .tsx if it’s a React component).
  • Fix the type errors that the compiler flags. You may need to annotate function parameters, return types, or imports/exports.

Repeat this process for other files or modules as you gain confidence. During the transition, any .js files in your include paths will also be compiled (thanks to allowJs: true).

3.2 Use JSDoc as a Bridge (Optional)

If you can’t rename certain files immediately (e.g., you’re on a time crunch), you can add JSDoc comments to your JavaScript files to introduce types gradually:

/**
 * @param {string} name
 * @param {number} age
 * @returns {string}
 */
function greet(name, age) {
  return `Hello, ${name}! You are ${age} years old.`;
}

TypeScript will read these JSDoc comments and provide type checking, though not as strictly as a full .ts file. This can be a stepping stone until you’re ready to rename the file.

Step 4: Dealing with Legacy Libraries

In a mature JavaScript codebase, you often rely on libraries that might not have complete type definitions. Fortunately, the TypeScript community maintains DefinitelyTyped, a huge repository of type definitions for popular libraries. You can install these types using:

4.1 Install Type Definitions

npm install --save-dev @types/

For example:

npm install --save-dev @types/lodash

4.2 When Types Are Missing

If the library doesn’t have existing type definitions, you have two main options:

Create a .d.ts file with your own type definitions. Place it somewhere like types/ in your project and reference it in tsconfig.json via "include".

Use any for a quick fix, but use it sparingly. Overuse of any defeats the purpose of TypeScript. As you learn more about how the library behaves, replace any with more precise types.

Using any can be a quick fix, but it effectively switches off TypeScript type-checking in those areas. Wherever possible, replace any with specific types to get the full benefit of TypeScript’s checks.

Step 5: Common Refactoring Patterns

Convert CommonJS to ES Modules

TypeScript works more smoothly with ES modules. If you see require() and module.exports, switch to import and export if possible.

Consolidate or Split Files

If a JavaScript file does too many things, it complicates type definitions. Splitting code into smaller modules makes type annotations simpler to manage.

Standardize Return Types

If a function can return multiple data types based on conditions, see if it can be simplified or if you need a Union Type (e.g., string | number).

Handle null and undefined

In strict mode, you’ll have to be explicit about nullable types. This encourages you to handle all code paths more cleanly.

Step 6: Linting and Formatting (Optional but Recommended)

While not strictly necessary for migrating to TypeScript, a well-integrated linting and formatting setup ensures code consistency:

Install ESLint

npm install --save-dev eslint @typescript-eslint/parser @typescript-eslint/eslint-plugin

Create or update your .eslintrc.json

{
  "parser": "@typescript-eslint/parser",
  "parserOptions": {
    "ecmaVersion": 2022,
    "sourceType": "module"
  },
  "extends": [
    "eslint:recommended",
    "plugin:@typescript-eslint/recommended"
  ],
  "rules": {
    // Add custom rules here
  }
}

Add a script in package.json:

{
  "scripts": {
    "lint": "eslint . --ext .ts,.js"
  }
}

Now you can quickly spot potential bugs and maintain a consistent style.

Step 7: Testing Your TypeScript Code

If you’re using a testing framework like Jest, you’ll need some additional configuration:

Install type definitions for your testing framework:

npm install --save-dev @types/jest

Configure Jest to understand TypeScript, often by adding ts-jest:

npm install --save-dev ts-jest
npx ts-jest config:init

Update jest.config.js (or .ts):

module.exports = {
  transform: {
    '^.+\\.tsx?$': 'ts-jest'
  }
};

Rename your test files to .test.ts or .test.tsx.

With this setup, test files can also benefit from TypeScript’s type checks.

Maintaining and Evolving Type Definitions

Once TypeScript is in place, keep your type definitions accurate. If you change a function’s expected argument, update the type accordingly. This may feel tedious at first, but it ensures the entire codebase stays consistent. As your application grows, accurate types actually reduce the chance of errors, acting as built-in documentation.

Avoiding and Resolving Pitfalls

Misconfiguring tsconfig.json: An incorrect module or target setting can cause TypeScript not to compile at all, or produce unexpected outputs. Keep an eye on error messages and double-check your config.

Using any too much: While any can help you get started quickly, overusing it negates the type-safety benefits. Use it sparingly, and replace it with more explicit types as soon as you can.

Ignoring errors: TypeScript is most beneficial when its warnings and errors are addressed. Suppressing errors with // @ts-ignore should be a temporary measure, not a permanent fix.

Version mismatches: Sometimes different parts of your toolchain (such as webpack, Babel, or other build tools) may conflict with each other. Keep your dependencies updated and consistent.

Staying Up-to-Date

TypeScript updates frequently with new releases that add features and performance improvements. Currently, TypeScript has reached its 5.x versions, which introduce new capabilities like improved decorators and constraint-aware generics. Keep an eye on the official TypeScript blog and release notes to upgrade in a timely manner. Modern build tools—like Vite or Next.js—often come pre-configured with TypeScript support and make it easier to stay current.

Updating usually just involves:

npm install --save-dev typescript@latest

Conclusion

Migrating a JavaScript codebase to TypeScript doesn’t have to be overwhelming. By creating a solid tsconfig.json, leveraging allowJs for an incremental transition, and carefully handling legacy libraries, you can steadily transform your project into a well-typed, more reliable codebase.

Whether it’s a small side project or a complex enterprise application, the type safety, autocomplete, and maintainability benefits of TypeScript will pay dividends in your development workflow. Embrace these best practices, iterate gradually, and soon you’ll be enjoying fewer runtime bugs and a more confident coding experience.

Happy coding, and welcome to a more type-safe future!

Start Project

Bring your vision to life with a custom website. Let's chat about your goals.

Let's Collab

Your next departure starts here

CONSOLE.LOG

CONSOLE.LOG

CONSOLE.LOG

CONSOLE.LOG

CONSOLE.LOG

CONSOLE.LOG

CONSOLE.LOG

CONSOLE.LOG

CONSOLE.LOG

CONSOLE.LOG

CONSOLE.LOG

CONSOLE.LOG