JavaScript to TypeScript Migration
MedMigrating a JavaScript codebase to TypeScript is a common real-world task and a popular interview topic. A successful migration happens incrementally — you configure TypeScript to coexist with JavaScript, gradually add types starting with the most critical paths, and progressively tighten strict checks. Understanding the migration strategy, common blockers, and tooling is essential for senior frontend roles.
Interactive Visualization
Key Points
- Enable `allowJs: true` and `checkJs: false` to let JS and TS coexist
- Rename files from `.js` to `.ts` one at a time, starting with leaf modules
- Use `// @ts-check` in JS files to enable TypeScript checking without renaming
- Start with `strict: false` and enable strict flags incrementally
- Create declaration files (`.d.ts`) for third-party JS modules without types
- Use `unknown` instead of `any` for untyped boundaries — it forces proper handling
- The migration order should be: utilities → types → business logic → components
Code Examples
Incremental tsconfig Setup
// tsconfig.json — Phase 1: Coexistence { "compilerOptions": { "target": "ES2020", "module": "ESNext", "moduleResolution": "bundler", "allowJs": true, // Allow .js files in the project "checkJs": false, // Don't type-check .js files yet "strict": false, // Start permissive "noImplicitAny": false, // Allow implicit any initially "esModuleInterop": true, "outDir": "./dist", "rootDir": "./src", "jsx": "react-jsx" }, "include": ["src/**/*"] } // Phase 2: Tighten gradually // "noImplicitAny": true // Enable after fixing most any types // "strictNullChecks": true // Enable after adding null checks // "strict": true // Final goal
Start with a permissive tsconfig that allows JS and TS to coexist. Gradually enable strict flags as you convert files and fix type errors. This prevents blocking the entire team.
Migration Patterns
// BEFORE: JavaScript module // utils/format.js export function formatCurrency(amount, currency) { return new Intl.NumberFormat('en-US', { style: 'currency', currency, }).format(amount) } // AFTER: TypeScript with explicit types // utils/format.ts interface FormatCurrencyOptions { locale?: string minimumFractionDigits?: number } export function formatCurrency( amount: number, currency: string, options: FormatCurrencyOptions = {} ): string { const { locale = 'en-US', minimumFractionDigits } = options return new Intl.NumberFormat(locale, { style: 'currency', currency, minimumFractionDigits, }).format(amount) } // Typing a JS module you cannot modify yet // types/legacy-lib.d.ts declare module 'legacy-analytics' { export function track(event: string, data?: Record<string, unknown>): void export function init(config: { apiKey: string }): void }
Convert files by adding type annotations to parameters and return types. Start with utility modules that have no dependencies, then work inward toward the core business logic.
Common Migration Blockers
// Blocker 1: Dynamic object shapes // JS pattern — add properties dynamically const config = {} config.host = 'localhost' // Error in TS: property does not exist // Fix: Define the interface upfront interface Config { host: string port: number debug?: boolean } const config: Config = { host: 'localhost', port: 3000 } // Blocker 2: Untyped event emitters // Fix: Type the event map interface AppEvents { userLogin: { userId: string } error: Error ready: void } // Blocker 3: Mixed module systems // Fix: Enable esModuleInterop for CommonJS interop // import express from 'express' // instead of require() // Blocker 4: Implicit this class Timer { seconds = 0 // start() { setInterval(function() { // this.seconds++ // Error: 'this' is any in regular functions // }, 1000) } start(): void { setInterval(() => { this.seconds++ // Arrow function preserves 'this' }, 1000) } }
The most common migration blockers are dynamic object shapes, untyped event emitters, mixed module systems, and implicit `this` in callbacks. Each has a standard TypeScript fix.
Common Mistakes
- Trying to migrate the entire codebase at once instead of incrementally
- Using `any` everywhere to make the build pass — defeats the purpose of migration
- Not setting up CI to run type checking — errors slip through
- Renaming files without actually adding types — `.ts` extension alone adds no safety
- Blocking the team by enabling strict mode before the codebase is ready
Interview Tips
- Describe a concrete migration strategy: start with leaf modules, work inward
- Mention the allowJs + checkJs coexistence approach for gradual adoption
- Know the common blockers and their fixes — interviewers want practical experience
- Explain the business case: migration reduces bugs, improves refactoring confidence, and helps onboarding