Migrate JavaScript to TypeScript — Complete Step-by-Step Guide
In this tutorial, you will learn about Migrate JavaScript to TypeScript. We cover key concepts, practical examples, and best practices to help you master this topic.
Migrating JavaScript to TypeScript is best done gradually — a proven strategy converts one file at a time while keeping the build green, using allowJs, checkJs, and strict mode flags to incrementally tighten type safety.
What You'll Learn
- Gradual migration strategy
- Configuration for mixed JS/TS codebases
- Converting files incrementally
- Handling third-party libraries
- Fixing common migration errors
- CI integration and quality gates
Why It Matters
Rewriting an entire JavaScript codebase to TypeScript in one shot is risky, time-consuming, and often abandoned halfway. A gradual migration — converting files one by one with strict mode enforcement — maintains velocity while improving type safety.
Real-World Use
The Doda Browser codebase migrated from JavaScript to TypeScript over 6 months, converting 200,000+ lines of code. The gradual strategy allowed teams to ship features during the migration — each sprint included type conversions alongside regular feature work.
Learning Path
flowchart LR A[Project: CLI Tool] --> B[Migration from JS] B --> C[Ecosystem Overview] B --> D[You Are Here] C --> E[What's Next]
Phase 1: Setup TypeScript in an Existing JS Project
Start by adding TypeScript without changing any files:
npm install --save-dev typescript
npx tsc --init
Configure for Mixed Codebase
{
"compilerOptions": {
"target": "ES2022",
"module": "Node16",
"moduleResolution": "Node16",
"strict": true,
"esModuleInterop": true,
"skipLibCheck": true,
"forceConsistentCasingInFileNames": true,
"resolveJsonModule": true,
"declaration": true,
"declarationMap": true,
"sourceMap": true,
"outDir": "./dist",
"rootDir": "./src",
"allowJs": true,
"checkJs": false
},
"include": ["src"]
}
The critical flags for migration:
allowJs: true— TypeScript processes.jsfilescheckJs: false— No Type Checking on JS files (yet)
Phase 2: Build and Verify
Add a build script and verify it works:
{
"scripts": {
"build": "tsc",
"typecheck": "tsc --noEmit"
}
}
npm run build
If the build succeeds, TypeScript is correctly configured for your project. If it fails, check that allowJs is enabled and all import paths resolve correctly.
Phase 3: Add Type Checking for JS Files
Enable checkJs: true to start detecting errors in JavaScript files:
{
"compilerOptions": {
"checkJs": true
}
}
This will report type errors in your .js files. Use // @ts-check at the top of individual files to enable checking selectively instead.
Fix Common Errors
// Before — JavaScript (will error with checkJs)
function greet(name) {
return `Hello, ${name.toUpperCase()}`;
// Error: name is possibly undefined
}
// After — add JSDoc type annotations
/**
* @param {string} name
* @returns {string}
*/
function greet(name) {
return `Hello, ${name.toUpperCase()}`;
}
Phase 4: Rename Files to .ts
Convert files one at a time. Start with utility files (no external dependencies) and work inward:
# Rename a single file
mv src/utils/format.js src/utils/format.ts
JSDoc to TypeScript Conversion
// Before — JavaScript with JSDoc
/**
* @param {{ id: string, name: string }} user
* @returns {string}
*/
function formatUser(user) {
return `${user.name} (${user.id})`;
}
// After — TypeScript
interface User {
id: string;
name: string;
}
function formatUser(user: User): string {
return `${user.name} (${user.id})`;
}
Handling Imports
When you rename a file, update imports everywhere it's referenced:
# Find all files importing the renamed module
rg "from './utils/format'" --files-with-matches
rg "require\('./utils/format'\)" --files-with-matches
Phase 5: The any Strategy
During migration, use any as a temporary escape hatch:
// Temporary — acceptable during migration
function processData(data: any): any {
return data.transform();
}
// Later — replace with proper types
interface DataInput {
value: number;
format: 'json' | 'xml';
}
interface DataOutput {
result: string;
timestamp: Date;
}
function processData(data: DataInput): DataOutput {
// Implement properly
return { result: String(data.value), timestamp: new Date() };
}
Tracking any Usage
# Count remaining any usage
rg '\bany\b' src/ --include '*.ts' | wc -l
# Track over time
# Week 1: 350 any
# Week 2: 280 any
# Week 3: 190 any
# ...
Phase 6: Handling Third-Party Libraries
Find or create type definitions for dependencies:
# Check if types exist
npm info @types/lodash
# Install types
npm install --save-dev @types/lodash
# If no types exist, create a declaration file
// src/types/legacy-library.d.ts
declare module 'legacy-library' {
export function doSomething(input: string): number;
export const VERSION: string;
}
For libraries without types, use declare module 'library-name' as a stopgap, then flesh out the types as you use the library.
Phase 7: Enable Strict Mode Incrementally
Don't enable all strict flags at once. Enable them one by one:
{
"compilerOptions": {
"strict": false,
"noImplicitAny": true,
"strictNullChecks": true,
"noImplicitReturns": true,
"noUnusedLocals": true
}
}
Order of enabling (recommended):
noImplicitAny— catches missing type annotationsstrictNullChecks— catches null/undefined accessnoImplicitReturns— catches missing return statementsnoUnusedLocals— cleans up dead codestrictFunctionTypes— strict function parameter checkingstrictBindCallApply— strict bind/call/apply typingstrictPropertyInitialization— class property initialization checks
Phase 8: CI Integration
Add TypeScript checking to your CI pipeline:
# .github/workflows/ci.yml
name: CI
on: [push, pull_request]
jobs:
typecheck:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: 22
- run: npm ci
- run: npm run typecheck
- run: npm run build
type-coverage:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- run: npm ci
- run: npx type-coverage
Type Coverage Tracking
npm install --save-dev type-coverage
npx type-coverage
Expected output:
Type coverage: 78.5% (2456/3128)
Track coverage weekly and set a minimum threshold:
{
"scripts": {
"type-coverage": "type-coverage --strict --at-least 80"
}
}
Common Mistakes
1. Trying to migrate everything at once
A full rewrite is risky and slow. Convert files incrementally. Use allowJs to keep JS files working alongside new TS files.
2. Not using allowJs and checkJs
Without these flags, TypeScript ignores .js files entirely, creating a broken intermediate state where imports fail.
3. Adding any and never removing it
Any is a migration aid, not a permanent solution. Track any usage and create a plan to eliminate it.
4. Forgetting to update import paths after rename
When utils.js becomes utils.ts, every import './utils' still works. But import './utils.js' needs to change to import './utils.ts' or remove the extension.
5. Not handling third-party types early
Unresolved module errors block compilation. Install @types/* packages or create declaration files early in the migration.
6. Ignoring strict mode until the end
Enabling strict mode at the end of migration creates a massive error wall. Enable strict flags incrementally during the migration.
7. Not involving the team
Migration affects everyone. Set team conventions for any usage, JSDoc annotations, and the order of file conversion.
Practice Questions
What does
allowJs: truedo in tsconfig? It tells TypeScript to Process.jsfiles alongside.tsfiles, allowing a mixed codebase during migration.What's the difference between
checkJs: trueand// @ts-check?checkJsenables checking globally.// @ts-checkenables it per file. Use// @ts-checkfor gradual adoption.How do you add types for a library that doesn't have them? Create a
.d.tsdeclaration file:declare module 'library-name' { export function fn(): void; }.What is
type-coverageand why is it useful? It measures what percentage of code has explicit types. Track it during migration to measure progress and enforce quality gates.Why enable strict flags one at a time? Enabling all at once creates hundreds or thousands of errors. Incremental enabling lets teams fix one category of errors at a time.
Challenge
Create a migration plan for a sample 10-file JavaScript project. Write a script that calculates the type coverage, identifies missing types, generates .d.ts stubs for third-party libraries, and tracks progress over time.
FAQ
{{< faq "What if my team doesn't want to use TypeScript?" >} TypeScript is optional per file in a mixed codebase. JS files remain untouched. New files and heavily modified files can be written in TypeScript. Let the migration happen organically. {{< /faq >}}
Mini Project
Execute a guided migration on a sample 5-file JavaScript project:
- Add TypeScript with
allowJs: true - Verify the build passes
- Enable
checkJs: trueand fix reported errors - Create
.d.tsdeclarations for any third-party libraries - Rename 2 source files to
.tsand add proper types - Enable
noImplicitAnyand fix errors - Measure type coverage before and after
- Configure CI to check types on pull requests
What's Next
You've learned how to migrate JavaScript to TypeScript. Now explore the broader TypeScript ecosystem with {{< ref "59-ecosystem-overview" >}}, or see what's next after mastering TypeScript with {{< ref "60-whats-next" >}}.
For a complete project example, check {{< ref "55-project-rest-api" >}}.
Built by the developers of DodaTech
Doda Browser, DodaZIP & Durga Antivirus Pro