Chapter 37: TypeScript Migration Guide
TypeScript Migration Guide very slowly, step-by-step, like we’re sitting together in Hyderabad with two laptops open, a cup of chai, and we’re actually migrating a small-to-medium project together right now.
Migrating a JavaScript project to TypeScript (or upgrading an old TypeScript project to modern TS + strict mode) is one of the most common and most valuable tasks developers do in 2025–2026.
The goal is usually:
- Catch bugs before they reach production
- Get excellent editor support (autocomplete, refactoring, go-to-definition)
- Make onboarding new team members 5–10× faster
- Gradually improve code quality without a big-bang rewrite
1. Two very different migration situations (you need to know which one you’re in)
| Situation | Typical project size | Difficulty | Recommended strategy in 2026 | Time estimate (rough) |
|---|---|---|---|---|
| Pure JavaScript → TypeScript | Small to large | Medium–Hard | Incremental / allowJs mode | 2 days – several months |
| Old TypeScript → Modern/Strict TS | Medium to very large | Medium | Gradual strict mode rollout | 1 week – several months |
Today we’re mostly going to focus on the first case (JS → TS), because that’s what most people search for when they say “migration guide”.
2. Recommended modern migration strategy in 2026 (incremental + safe)
The winning pattern used by almost every serious team right now:
- Add TypeScript to the project without changing almost any code
- Enable allowJs + checkJs so you get some safety on .js files
- Start converting files one by one (or folder by folder)
- Gradually turn on stricter flags
- Add // @ts-expect-error or // @ts-ignoreonly as temporary escape hatches
- Use tsc –noEmit in CI from day 1 (zero runtime risk)
3. Step-by-step realistic migration guide (2026 style)
Step 1: Add TypeScript to the project
|
0 1 2 3 4 5 6 7 8 |
npm install --save-dev typescript @types/node # If React: npm install --save-dev @types/react @types/react-dom |
Create minimal tsconfig.json
|
0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 |
{ "compilerOptions": { "target": "ES2022", "module": "ESNext", "moduleResolution": "bundler", // or "NodeNext" if pure Node "allowJs": true, // ← very important! "checkJs": true, // ← gives you basic errors on .js files "noEmit": true, // we don't want tsc to output .js "skipLibCheck": true, "strict": false, // start loose — tighten later "esModuleInterop": true, "jsx": "react-jsx", // if React "baseUrl": ".", "paths": { "@/*": ["src/*"] } }, "include": ["src/**/*"], "exclude": ["node_modules", "dist", "build"] } |
Step 2: Rename one important file to .ts (start small)
Common safe starting points:
- Entry file (index.js → index.ts)
- Utility file (utils.js → utils.ts)
- API client / service file
- Small pure function file
Do NOT start with:
- React component files (they have JSX → more complicated)
- Files that use require() heavily (CommonJS interop pain)
- Files full of any / third-party data
Example: rename utils/format.js → utils/format.ts
|
0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
// Before (format.js) export function formatDate(date) { return date.toISOString(); } // After (format.ts) — minimal change export function formatDate(date) { return date.toISOString(); // ← tsc will complain: date is any } |
Step 3: Add the smallest possible types (baby steps)
|
0 1 2 3 4 5 6 7 8 9 |
// format.ts — first improvement export function formatDate(date: Date): string { return date.toISOString(); } |
Now if someone calls it wrong → error immediately.
Step 4: Handle third-party libraries
|
0 1 2 3 4 5 6 7 8 |
# If the library has built-in types → nothing to do # If not → install @types npm install --save-dev @types/lodash @types/axios |
If a library has no types at all (very rare in 2026):
|
0 1 2 3 4 5 6 7 8 9 |
// declarations.d.ts declare module 'some-obscure-lib' { export function doSomething(value: any): any; } |
Step 5: Gradually tighten tsconfig (very important order)
Start loose → tighten one flag at a time (commit between each):
|
0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 |
// Level 1 (start here) "strict": false // Level 2 "noImplicitAny": true, "strictNullChecks": true, // Level 3 "strict": true, // turns on almost everything // Level 4 (very strict & modern) "exactOptionalPropertyTypes": true, "noPropertyAccessFromIndexSignature": true, "noUncheckedIndexedAccess": true, "noFallthroughCasesInSwitch": true, "verbatimModuleSyntax": true |
Each time you turn on a new strict flag → fix the new errors that appear.
Step 6: Use escape hatches wisely (temporary only!)
|
0 1 2 3 4 5 6 7 8 9 10 11 12 |
// 1. Quick ignore (use sparingly) const data = response.data as any; // 2. Better — expect error (documented) const legacyApiResult = oldApiCall(); // @ts-expect-error — waiting for proper types from backend team const userId = legacyApiResult.userId; |
Rule of thumb 2026: Aim for zero @ts-ignore and very few @ts-expect-error in new/hot files.
7. Real example — migrating a small Express API (very common case)
Before (pure JS)
|
0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
// server.js const express = require('express'); const app = express(); app.get('/users/:id', (req, res) => { const id = req.params.id; // fake DB const user = { id, name: 'Rahul' }; res.json(user); }); app.listen(4000); |
Migration steps (realistic order)
- npm install –save-dev typescript @types/express @types/node
- Create tsconfig.json (allowJs + checkJs)
- Rename server.js → server.ts
- Change requires → imports
|
0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
// server.ts — step 4 import express from 'express'; const app = express(); app.get('/users/:id', (req, res) => { const id = req.params.id; // ← error: id is string | string[] const user = { id, name: 'Rahul' }; res.json(user); }); app.listen(4000); |
- Fix the error (add types)
|
0 1 2 3 4 5 6 7 8 9 10 11 12 |
import express, { Request, Response } from 'express'; app.get('/users/:id', (req: Request<{ id: string }>, res: Response) => { const id = Number(req.params.id); // safe conversion const user = { id, name: 'Rahul' }; res.json(user); }); |
- Later — move route handlers to separate typed files, add interfaces, etc.
8. Quick checklist — modern 2026 migration success signs
- tsc –noEmit passes in CI from day 1
- allowJs: true + checkJs: true → you get some value even on .js files
- You convert 1–5 files per day (not hundreds at once)
- You tighten strict gradually (never flip everything at once)
- You use satisfies, as const, literal types more and more
- After ~30–50% converted → you turn on most strict flags
- You remove almost all @ts-ignore within 3–6 months
Your next realistic step (do today)
- Pick one small utility file in your project
- Rename .js → .ts
- Add minimal parameter/return types
- Run tsc –noEmit → fix the first 3–5 errors
- Commit → feel the dopamine hit
Which part feels most relevant / scary for your current project?
Want to:
- Walk through migrating a React component file
- Handle CommonJS → ESM migration at the same time
- Deal with third-party JS libraries without types
- Set up CI + pre-commit hooks for migration
- Plan long-term strict mode rollout
Just tell me — we’ll go deeper right there! 😄
