Every migration guide starts with "just rename .js to .ts." That's like saying "just land the rocket" - technically correct but useless when you hit real problems. After helping dozens of teams migrate codebases from 10K to 500K lines, here's what actually happens and how to fix it.
Migration Timeline Reality Check
Small projects (< 5K lines): anywhere from a few days to a month depending on how much weird JS you have
Medium projects (5K-50K lines): 2-4 months with incremental approach, longer if you hit framework integration hell
Large projects (50K+ lines): 6-12 months minimum, plan for longer when you discover all the dynamic code
That GitHub project you saw migrate 100K lines in "2 weeks"? They had 8 engineers working full-time on migration and nothing else. Plan accordingly. Check real migration timelines from companies like Airbnb and Slack.
The Fatal Migration Mistakes
Teams try to migrate everything at once. The TypeScript compiler throws 500+ errors and your engineers will want to murder you. Half the team gives up and wants to revert.
The Fix: Do it file by file. Start with the easy stuff - utility functions, constants. Work your way up. Don't mix type fixes with feature work or you'll go insane.
Mistake 2: Fighting Every Error Right Away
You rename 50 files to .ts and suddenly have 200 compiler errors. Everyone tries to fix every single error immediately. Development stops for weeks and your PM starts asking uncomfortable questions.
The Fix: Use @ts-ignore
and any
everywhere at first. Get the damn thing compiling, then fix types gradually. Better to ship with some any
types than get stuck for months. One team I consulted spent 3 weeks fixing type errors in their date formatting utils while their customers waited for a critical bug fix. They could have shipped with // @ts-ignore
and fixed the types later.
Mistake 3: Perfect Types from Day One
Teams enable strict: true
immediately because "we want to do TypeScript properly." Compiler errors multiply exponentially. Team burns out fighting generic type constraints nobody understands.
The Fix: Start with strict: false
. TypeScript should make your life easier, not turn you into a type theorist.
Step-by-Step Migration Process That Actually Works
Phase 1: Preparation (1-2 weeks)
Audit your dependencies: Run
npm ls
and identify packages without@types
support. You'll need to write basic declarations for these. Check DefinitelyTyped first.Set up build tooling: Configure TypeScript compiler to work alongside your existing JavaScript build. Enable
allowJs: true
so you can mix .js and .ts files during migration. The compiler options reference explains each setting.Create migration-friendly tsconfig.json: Start with recommended tsconfig bases for your environment:
{
"compilerOptions": {
"target": "es2018",
"module": "esnext",
"allowJs": true,
"outDir": "./dist",
"strict": false,
"noImplicitReturns": false,
"skipLibCheck": true
},
"include": ["src/**/*"],
"exclude": ["node_modules", "dist"]
}
Phase 2: File Conversion (2-8 weeks)
Start with utility files: Pure functions, constants, and helper modules convert easily. These usually have minimal dependencies and clear inputs/outputs.
Move to data models: Convert your interfaces, API response types, and configuration objects. These provide type safety for the rest of your codebase.
Tackle business logic: Convert your core application logic. This is where you'll hit most of the complex type issues.
Finish with integration files: Convert files that integrate with external APIs, frameworks, or legacy systems last. These often need the most
any
types.
Phase 3: Type Safety Improvements (Ongoing)
Enable strict checks gradually: Turn on one strict compiler option per week. Fix the errors it surfaces before enabling the next one. The strict mode guide explains each option.
Replace
any
types systematically: Use TypeScript'snoImplicitAny
error reports to find files that need type improvements. Type coverage tools help track progress.Add tests for type coverage: Use tools like typescript-coverage-report to track your progress visually.
The Top 5 Errors That Kill Migrations
Error 1: Cannot find module './relative-path'
What it means: TypeScript can't find your internal modules, usually because of path resolution differences.
The fix:
// tsconfig.json
{
"compilerOptions": {
"baseUrl": "./src",
"paths": {
"@/*": ["*"],
"@components/*": ["components/*"]
}
}
}
Update your imports to use the configured paths:
// Before
import Button from '../../../components/Button'
// After
import Button from '@components/Button'
Error 2: Property 'foo' does not exist on type '{}'
What it means: TypeScript inferred an empty object type, but you're trying to access properties on it.
Quick fix: Add a type annotation:
// Broken
const user = {};
user.name = "John"; // Error!
// Fixed
const user: { name?: string } = {};
user.name = "John"; // Works
Better fix: Define proper interfaces:
interface User {
name?: string;
email?: string;
}
const user: User = {};
user.name = "John"; // Type-safe
Error 3: Argument of type 'string | undefined' is not assignable to parameter of type 'string'
What it means: Strict null checks caught a potential undefined value.
The fix: Handle the undefined case explicitly:
// Broken
function processName(name: string | undefined) {
return name.toUpperCase(); // Error!
}
// Fixed
function processName(name: string | undefined) {
if (!name) return "";
return name.toUpperCase(); // Type-safe
}
Error 4: Cannot find module 'some-library' or its corresponding type declarations
What it means: No type definitions available for this library.
Quick fix: Create a basic declaration file:
// types/some-library.d.ts
declare module 'some-library' {
export function someFunction(arg: any): any;
}
Better fix: Install the official types or write proper declarations:
npm install --save-dev @types/some-library
Error 5: Expected 2 arguments, but got 1
What it means: Function signature mismatch between JavaScript usage and TypeScript types.
The fix: Check if you need to make parameters optional:
// Library expects 2 args but you only pass 1
function apiCall(url: string, options?: RequestInit) {
// Make second parameter optional with ?
}
Advanced Migration Challenges
Dealing with this
Context Issues
JavaScript's dynamic this
binding breaks TypeScript's type system. Convert problematic patterns:
// Problematic JavaScript pattern
const handler = {
data: [],
onClick: function() {
this.data.push('item'); // `this` type is unclear
}
};
// TypeScript-friendly pattern
class Handler {
private data: string[] = [];
onClick = () => {
this.data.push('item'); // `this` is properly typed
}
}
Framework-Specific Issues
React: Event handlers need explicit types:
// Before
const handleClick = (e) => { /* ... */ }
// After
const handleClick = (e: React.MouseEvent<HTMLButtonElement>) => { /* ... */ }
Node.js: Module resolution requires explicit configuration:
{
"compilerOptions": {
"module": "commonjs",
"esModuleInterop": true
}
}
Using Automated Migration Tools
ts-migrate (Airbnb's Tool)
Best for large React codebases. Handles 80% of mechanical conversion but leaves you with lots of any
types to clean up. The ts-migrate GitHub repo has detailed usage instructions.
npx ts-migrate-full <project-folder>
Handles React prop types conversion and renames files automatically. But it generates tons of any
types and doesn't handle complex type relationships. Read Airbnb's migration story for context.
AI-Assisted Migration
Tools like GitHub Copilot can help with individual file conversions but struggle with large-scale architectural decisions.
Best practice: Use AI for converting utility functions and simple components. Handle complex business logic manually. The TypeScript conversion patterns guide shows common transformations.
Team Management During Migration
Set Clear Expectations
- Migration will slow down feature development by 20-30% initially
- First month is mostly about getting things compiling, not perfect types
- Type coverage improves gradually over 3-6 months
Create Migration Guidelines
- No mixing migration work with features: Keep migration PRs separate from business logic changes
- Review process: All migration PRs need TypeScript-experienced reviewer
- Progress tracking: Weekly reports on files converted and error count reduction
Handle Team Resistance
- JavaScript purists: Show them refactoring safety and IDE autocomplete benefits
- Deadline pressure: Start migration during low-feature periods
- Learning curve: Pair experienced TypeScript developers with JavaScript experts
The migration is worth it - every team that completes it reports better code quality, faster debugging, and more confident refactoring. But plan for the reality, not the marketing promises.