← Back to Blog

JavaScript to TypeScript Migration: The Definitive Playbook for 2026

TypeScript won. The State of JS survey shows adoption climbing every year, and the holdouts are running out of arguments. "We don't need types" doesn't hold up after your third production incident caused by undefined is not a function.

The good news: JS → TS is the gentlest migration in the industry. TypeScript is a superset of JavaScript, so you can adopt it file by file, function by function. The bad news: "gentle" doesn't mean "trivial." A 500-file React application has a lot of implicit contracts hiding behind untyped function signatures.

Here's how to do it right.

Phase 1: Infrastructure Setup

Before touching a single .js file, set up your TypeScript infrastructure.

// tsconfig.json — start permissive
{
  "compilerOptions": {
    "target": "ES2020",
    "module": "commonjs",
    "allowJs": true,
    "checkJs": false,
    "strict": false,
    "noEmit": true,
    "esModuleInterop": true,
    "skipLibCheck": true
  },
  "include": ["src/**/*"]
}

The critical setting is allowJs: true. This lets .ts and .js files coexist, so you can migrate incrementally without breaking the build.

Install type definitions for your dependencies:

npm install --save-dev typescript @types/react @types/node @types/express

Phase 2: Automated File Conversion

The mechanical part of JS → TS conversion is well-understood:

  • Rename .js.ts (or .jsx.tsx)
  • Add type annotations to function parameters and return types
  • Convert require() to import statements where appropriate
  • Add interface definitions for object shapes that appear repeatedly

Before (JavaScript):

function calculateTotal(items, taxRate) {
  const subtotal = items.reduce((sum, item) => sum + item.price * item.qty, 0);
  return subtotal * (1 + taxRate);
}

After (TypeScript):

interface LineItem {
  price: number;
  qty: number;
}

function calculateTotal(items: LineItem[], taxRate: number): number {
  const subtotal = items.reduce((sum, item) => sum + item.price * item.qty, 0);
  return subtotal * (1 + taxRate);
}

This kind of mechanical conversion is where automated tools shine. JS → TS is rated quality 3 (excellent) on platforms like B&G CodeFoundry — the highest confidence level — because the languages are so structurally close. An automated first pass handles the renaming, basic type annotations, and import restructuring. Developers focus on refining the types rather than doing the grunt work.

Phase 3: Handling any

Your automated conversion will produce any types wherever it can't infer the correct type. This is fine as a starting point — it's the TypeScript equivalent of TODO comments. But you need a strategy for eliminating them.

Gradual typing: Leave any in stable, rarely-changed code. Prioritize typing at module boundaries (function signatures, exported interfaces, API response types). Internal implementation details can stay loosely typed longer.

The unknown upgrade: When you revisit an any, first change it to unknown. This forces explicit type narrowing before use, catching errors without requiring you to know the full type immediately.

// Step 1: any (compiles but unsafe)
function parseConfig(raw: any): any { ... }

// Step 2: unknown (forces narrowing)
function parseConfig(raw: unknown): Config {
  if (typeof raw !== 'object' || raw === null) throw new Error('Invalid');
  // now TypeScript knows raw is an object
}

Phase 4: Enable Strict Mode (Gradually)

Don't flip "strict": true on day one across the whole project. Enable strict checks one at a time:

  1. "noImplicitAny": true — the biggest win. Forces explicit typing of all parameters.
  2. "strictNullChecks": true — catches null/undefined access. The single most valuable TypeScript feature.
  3. "strictFunctionTypes": true — catches covariance bugs in function types.

Enable each flag, fix the resulting errors in a focused PR, and move to the next. Airbnb's TypeScript migration followed this incremental strictness approach across their frontend.

What Can Silently Break

TypeScript migration introduces subtle behavior changes if you're not careful:

Enum values. TypeScript enums compile to objects with reverse mappings. If your JS code was using plain string constants, switching to enums changes the runtime representation.

Class fields. TypeScript class field declarations with useDefineForClassFields: true (the default in modern TS) use Object.defineProperty instead of simple assignment. This can break code that relies on prototype chain behavior.

Import order. Switching from require() to import can change module initialization order if your code has side effects during import.

Test thoroughly. Run your existing test suite after each batch of conversions. If tests pass, the conversion preserved behavior. If they fail, you've found the gaps.

The Finish Line

A complete TypeScript migration for a 500-file project typically takes 2-6 weeks with automated conversion handling the mechanical phase. The timeline depends almost entirely on how strict you want to go and how many any types you're willing to tolerate in the short term.

The ROI is clear: Stripe's engineering team reported that their TypeScript migration of the Dashboard reduced production null-reference errors by over 60%. The types pay for themselves.


References: Airbnb's TypeScript migration; Stripe Dashboard TS conversion; State of JS 2024-2025 survey data; TypeScript 5.x release notes.