Migration: JS to TS

Difficulty: Intermediate

Question

How would you migrate a large JavaScript codebase to TypeScript? What is the recommended strategy?

Answer

Migrating a large JS codebase to TypeScript should be done incrementally, not all at once. The recommended strategy:

1. Setup phase: Add tsconfig with allowJs: true and checkJs: false. Both .js and .ts files coexist. 2. Infrastructure first: Migrate config files, build scripts, and utilities first. 3. Strict at the boundary: Enable strict for new .ts files, allow loose typing for existing .js files. 4. Bottom-up migration: Start with leaf modules (no dependencies), then work up to modules with more dependencies. 5. Type coverage tracking: Gradually increase coverage, aiming for 100% strict over time.

Key enablers: allowJs, checkJs, JSDoc type annotations, @ts-ignore/@ts-expect-error for temporary suppressions.

Code examples

Migration tsconfig Setup

// Phase 1: Initial tsconfig.json
{
  "compilerOptions": {
    "target": "ES2020",
    "module": "ESNext",
    "moduleResolution": "bundler",

    // Migration enablers
    "allowJs": true,             // Allow .js files in project
    "checkJs": false,            // Don't type-check .js files yet
    "strict": false,             // Relaxed for migration
    "noImplicitAny": false,      // Allow implicit any during migration
    "skipLibCheck": true,

    "outDir": "./dist",
    "declaration": true
  },
  "include": ["src//*"]
}

// Phase 2: Enable checking gradually
// "checkJs": true       // Type-check JS files using JSDoc
// "noImplicitAny": true // After most files are typed

// Phase 3: Full strict mode
// "strict": true
// "allowJs": false       // Remove when all .js files are converted

Start with allowJs: true and strict: false. This lets .js and .ts files coexist. Gradually enable strict options as more files are converted.

JSDoc Types for JS Files

// utils.js - Add types via JSDoc without renaming to .ts

/
 * @typedef {Object} User
 * @property {number} id
 * @property {string} name
 * @property {string} email
 * @property {boolean} [isActive] - optional property
 */

/
 * Fetch a user by ID
 * @param {number} id - The user ID
 * @returns {Promise<User>} The user object
 */
async function getUser(id) {
  const res = await fetch(`/api/users/${id}`);
  return res.json();
}

/
 * @template T
 * @param {T[]} items
 * @param {(item: T) => boolean} predicate
 * @returns {T | undefined}
 */
function findItem(items, predicate) {
  return items.find(predicate);
}

// With checkJs: true, TypeScript validates these JSDoc types
// This lets you add type safety without renaming files

JSDoc annotations provide type safety in .js files without renaming. With checkJs: true, TypeScript validates JSDoc types. This is perfect for gradual migration.

Migration Workflow

// Step 1: Create shared types file
// src/types.ts
export interface User {
  id: number;
  name: string;
  email: string;
  role: 'admin' | 'user';
}

export interface ApiResponse<T> {
  data: T;
  error?: string;
  status: number;
}

// Step 2: Rename leaf module .js -> .ts and add types
// src/utils/format.ts (was format.js)
export function formatDate(date: Date): string {
  return date.toISOString().split('T')[0];
}

export function formatCurrency(amount: number, currency: string = 'USD'): string {
  return new Intl.NumberFormat('en-US', { style: 'currency', currency }).format(amount);
}

// Step 3: Use @ts-expect-error for known issues (tracked for fix)
function legacyFunction(data: any) {
  // @ts-expect-error - TODO: Fix when user module is migrated
  return data.process();
}

// Step 4: Track migration progress
// npx type-coverage   // Shows percentage of typed code
// npx type-coverage --detail // Shows untyped locations

// Migration checklist:
// [x] src/utils/format.ts       (100% typed)
// [x] src/utils/validation.ts   (100% typed)
// [ ] src/services/user.js      (JSDoc only)
// [ ] src/services/payment.js   (untyped)
// [ ] src/routes/api.js         (untyped)

Migrate bottom-up: utilities first, then services, then routes. Use @ts-expect-error (not @ts-ignore) for known issues so TypeScript errors when the underlying issue is fixed.

Key points

Concepts covered

Migration Strategy, allowJs, Gradual Adoption, Type Coverage, JSDoc Types