Skip to content

Migrate JavaScript to TypeScript — Complete Step-by-Step Guide

DodaTech Updated 2026-06-28 8 min read

In this tutorial, you will learn about Migrate JavaScript to TypeScript. We cover key concepts, practical examples, and best practices to help you master this topic.

Migrating JavaScript to TypeScript is best done gradually — a proven strategy converts one file at a time while keeping the build green, using allowJs, checkJs, and strict mode flags to incrementally tighten type safety.

What You'll Learn

  • Gradual migration strategy
  • Configuration for mixed JS/TS codebases
  • Converting files incrementally
  • Handling third-party libraries
  • Fixing common migration errors
  • CI integration and quality gates

Why It Matters

Rewriting an entire JavaScript codebase to TypeScript in one shot is risky, time-consuming, and often abandoned halfway. A gradual migration — converting files one by one with strict mode enforcement — maintains velocity while improving type safety.

Real-World Use

The Doda Browser codebase migrated from JavaScript to TypeScript over 6 months, converting 200,000+ lines of code. The gradual strategy allowed teams to ship features during the migration — each sprint included type conversions alongside regular feature work.

Learning Path

flowchart LR
  A[Project: CLI Tool] --> B[Migration from JS]
  B --> C[Ecosystem Overview]
  B --> D[You Are Here]
  C --> E[What's Next]

Phase 1: Setup TypeScript in an Existing JS Project

Start by adding TypeScript without changing any files:

npm install --save-dev typescript
npx tsc --init

Configure for Mixed Codebase

{
  "compilerOptions": {
    "target": "ES2022",
    "module": "Node16",
    "moduleResolution": "Node16",
    "strict": true,
    "esModuleInterop": true,
    "skipLibCheck": true,
    "forceConsistentCasingInFileNames": true,
    "resolveJsonModule": true,
    "declaration": true,
    "declarationMap": true,
    "sourceMap": true,
    "outDir": "./dist",
    "rootDir": "./src",
    "allowJs": true,
    "checkJs": false
  },
  "include": ["src"]
}

The critical flags for migration:

  • allowJs: true — TypeScript processes .js files
  • checkJs: false — No Type Checking on JS files (yet)

Phase 2: Build and Verify

Add a build script and verify it works:

{
  "scripts": {
    "build": "tsc",
    "typecheck": "tsc --noEmit"
  }
}
npm run build

If the build succeeds, TypeScript is correctly configured for your project. If it fails, check that allowJs is enabled and all import paths resolve correctly.

Phase 3: Add Type Checking for JS Files

Enable checkJs: true to start detecting errors in JavaScript files:

{
  "compilerOptions": {
    "checkJs": true
  }
}

This will report type errors in your .js files. Use // @ts-check at the top of individual files to enable checking selectively instead.

Fix Common Errors

// Before — JavaScript (will error with checkJs)
function greet(name) {
  return `Hello, ${name.toUpperCase()}`;
  // Error: name is possibly undefined
}

// After — add JSDoc type annotations
/**
 * @param {string} name
 * @returns {string}
 */
function greet(name) {
  return `Hello, ${name.toUpperCase()}`;
}

Phase 4: Rename Files to .ts

Convert files one at a time. Start with utility files (no external dependencies) and work inward:

# Rename a single file
mv src/utils/format.js src/utils/format.ts

JSDoc to TypeScript Conversion

// Before — JavaScript with JSDoc
/**
 * @param {{ id: string, name: string }} user
 * @returns {string}
 */
function formatUser(user) {
  return `${user.name} (${user.id})`;
}

// After — TypeScript
interface User {
  id: string;
  name: string;
}

function formatUser(user: User): string {
  return `${user.name} (${user.id})`;
}

Handling Imports

When you rename a file, update imports everywhere it's referenced:

# Find all files importing the renamed module
rg "from './utils/format'" --files-with-matches
rg "require\('./utils/format'\)" --files-with-matches

Phase 5: The any Strategy

During migration, use any as a temporary escape hatch:

// Temporary — acceptable during migration
function processData(data: any): any {
  return data.transform();
}

// Later — replace with proper types
interface DataInput {
  value: number;
  format: 'json' | 'xml';
}

interface DataOutput {
  result: string;
  timestamp: Date;
}

function processData(data: DataInput): DataOutput {
  // Implement properly
  return { result: String(data.value), timestamp: new Date() };
}

Tracking any Usage

# Count remaining any usage
rg '\bany\b' src/ --include '*.ts' | wc -l

# Track over time
# Week 1: 350 any
# Week 2: 280 any
# Week 3: 190 any
# ...

Phase 6: Handling Third-Party Libraries

Find or create type definitions for dependencies:

# Check if types exist
npm info @types/lodash

# Install types
npm install --save-dev @types/lodash

# If no types exist, create a declaration file
// src/types/legacy-library.d.ts
declare module 'legacy-library' {
  export function doSomething(input: string): number;
  export const VERSION: string;
}

For libraries without types, use declare module 'library-name' as a stopgap, then flesh out the types as you use the library.

Phase 7: Enable Strict Mode Incrementally

Don't enable all strict flags at once. Enable them one by one:

{
  "compilerOptions": {
    "strict": false,
    "noImplicitAny": true,
    "strictNullChecks": true,
    "noImplicitReturns": true,
    "noUnusedLocals": true
  }
}

Order of enabling (recommended):

  1. noImplicitAny — catches missing type annotations
  2. strictNullChecks — catches null/undefined access
  3. noImplicitReturns — catches missing return statements
  4. noUnusedLocals — cleans up dead code
  5. strictFunctionTypes — strict function parameter checking
  6. strictBindCallApply — strict bind/call/apply typing
  7. strictPropertyInitialization — class property initialization checks

Phase 8: CI Integration

Add TypeScript checking to your CI pipeline:

# .github/workflows/ci.yml
name: CI
on: [push, pull_request]
jobs:
  typecheck:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with:
          node-version: 22
      - run: npm ci
      - run: npm run typecheck
      - run: npm run build

  type-coverage:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - run: npm ci
      - run: npx type-coverage

Type Coverage Tracking

npm install --save-dev type-coverage
npx type-coverage

Expected output:

Type coverage: 78.5% (2456/3128)

Track coverage weekly and set a minimum threshold:

{
  "scripts": {
    "type-coverage": "type-coverage --strict --at-least 80"
  }
}

Common Mistakes

1. Trying to migrate everything at once

A full rewrite is risky and slow. Convert files incrementally. Use allowJs to keep JS files working alongside new TS files.

2. Not using allowJs and checkJs

Without these flags, TypeScript ignores .js files entirely, creating a broken intermediate state where imports fail.

3. Adding any and never removing it

Any is a migration aid, not a permanent solution. Track any usage and create a plan to eliminate it.

4. Forgetting to update import paths after rename

When utils.js becomes utils.ts, every import './utils' still works. But import './utils.js' needs to change to import './utils.ts' or remove the extension.

5. Not handling third-party types early

Unresolved module errors block compilation. Install @types/* packages or create declaration files early in the migration.

6. Ignoring strict mode until the end

Enabling strict mode at the end of migration creates a massive error wall. Enable strict flags incrementally during the migration.

7. Not involving the team

Migration affects everyone. Set team conventions for any usage, JSDoc annotations, and the order of file conversion.

Practice Questions

  1. What does allowJs: true do in tsconfig? It tells TypeScript to Process .js files alongside .ts files, allowing a mixed codebase during migration.

  2. What's the difference between checkJs: true and // @ts-check? checkJs enables checking globally. // @ts-check enables it per file. Use // @ts-check for gradual adoption.

  3. How do you add types for a library that doesn't have them? Create a .d.ts declaration file: declare module 'library-name' { export function fn(): void; }.

  4. What is type-coverage and why is it useful? It measures what percentage of code has explicit types. Track it during migration to measure progress and enforce quality gates.

  5. Why enable strict flags one at a time? Enabling all at once creates hundreds or thousands of errors. Incremental enabling lets teams fix one category of errors at a time.

Challenge

Create a migration plan for a sample 10-file JavaScript project. Write a script that calculates the type coverage, identifies missing types, generates .d.ts stubs for third-party libraries, and tracks progress over time.

FAQ

How long does a JavaScript to TypeScript migration take?

For a typical 50,000-line codebase with 5 developers, expect 2-4 months. Smaller projects can take 1-2 weeks. The gradual approach lets you ship features during migration.

Should I use JSDoc types or convert to .ts files?

JSDoc is a valid intermediate step. For long-term maintainability, rename to .ts and use TypeScript syntax. JSDoc is useful when you can't rename files (legacy build tools).

{{< faq "What if my team doesn't want to use TypeScript?" >} TypeScript is optional per file in a mixed codebase. JS files remain untouched. New files and heavily modified files can be written in TypeScript. Let the migration happen organically. {{< /faq >}}

How do I handle dynamic imports and require() calls?

TypeScript supports dynamic import() natively. For require(), use import x = require('x') syntax or enable esModuleInterop and use standard imports.

What's the biggest risk during migration?

Scope creep. Teams try to refactor code while converting to TypeScript. Keep migration separate from Refactoring — change the types first, then refactor.

Do I need to rewrite my tests?

Tests benefit from types too. Convert test files after the source files they test. Vitest and Jest both support TypeScript natively.

Mini Project

Execute a guided migration on a sample 5-file JavaScript project:

  1. Add TypeScript with allowJs: true
  2. Verify the build passes
  3. Enable checkJs: true and fix reported errors
  4. Create .d.ts declarations for any third-party libraries
  5. Rename 2 source files to .ts and add proper types
  6. Enable noImplicitAny and fix errors
  7. Measure type coverage before and after
  8. Configure CI to check types on pull requests

What's Next

You've learned how to migrate JavaScript to TypeScript. Now explore the broader TypeScript ecosystem with {{< ref "59-ecosystem-overview" >}}, or see what's next after mastering TypeScript with {{< ref "60-whats-next" >}}.

For a complete project example, check {{< ref "55-project-rest-api" >}}.

Built by the developers of DodaTech

Doda Browser, DodaZIP & Durga Antivirus Pro