18.5 JS → TS Migration — Incremental Transition Strategy
Migration Strategy Overview
Incremental Migration (recommended):
Stage 1: @ts-check + JSDoc (type checking without code changes)
Stage 2: allowJs + checkJs (mix JS and TS)
Stage 3: Convert files one by one: .js → .ts
Stage 4: Enable strict mode
Stage 5: Complete type declarations
Full Migration (small projects):
Change all .js → .ts + use any for initial types
→ Progressively remove any
Stage 1: @ts-check and JSDoc
// @ts-check
// Add to top of file → enables type checking in VS Code
/**
* @param {string} name
* @param {number} age
* @returns {{ name: string; age: number; createdAt: Date }}
*/
function createUser(name, age) {
return { name, age, createdAt: new Date() }
}
/**
* @typedef {Object} Product
* @property {string} id
* @property {string} name
* @property {number} price
* @property {boolean} [inStock]
*/
/**
* @param {Product[]} products
* @returns {Product[]}
*/
function filterInStock(products) {
return products.filter(p => p.inStock)
}
Stage 2: allowJs + checkJs Configuration
// tsconfig.json
{
"compilerOptions": {
"allowJs": true, // Allow JS files
"checkJs": true, // Type check JS files too
"strict": false, // Disable strict initially
"noImplicitAny": false, // Allow implicit any
"outDir": "./dist",
"rootDir": "./src"
},
"include": ["src/**/*"] // Include both .js and .ts
}
Stage 3: Per-File Conversion
# File extension rename script
find src -name "*.js" -not -path "*/node_modules/*" | \
xargs -I {} bash -c 'mv "$1" "${1%.js}.ts"' _ {}
# Or one file at a time
mv src/utils/helpers.js src/utils/helpers.ts
// Early in conversion — quickly pass with any
// src/legacy/user.ts
export function processUser(user: any): any {
return {
...user,
displayName: `${user.firstName} ${user.lastName}`,
}
}
// Incremental improvement — remove any
export interface LegacyUser {
firstName: string
lastName: string
email: string
}
export interface ProcessedUser extends LegacyUser {
displayName: string
}
export function processUser(user: LegacyUser): ProcessedUser {
return {
...user,
displayName: `${user.firstName} ${user.lastName}`,
}
}
Stage 4: Incremental strict Mode Activation
// tsconfig.json — enable options one by one
{
"compilerOptions": {
// Start with the most basic
"noImplicitAny": true, // Stage 1: force explicit any
"strictNullChecks": true, // Stage 2: null checks
"strictFunctionTypes": true, // Stage 3: strict function types
"strictPropertyInitialization": true, // Stage 4: property initialization
"noImplicitThis": true, // Stage 5: this types
// "strict": true // All combined — enable last
}
}
Legacy Pattern TypeScript Conversion
// Legacy JS patterns → TypeScript conversion examples
// 1. Prototype pattern without classes
// Before (JS)
function Animal(name) {
this.name = name
}
Animal.prototype.speak = function() {
return `${this.name} makes a noise.`
}
// After (TS)
class Animal {
constructor(public readonly name: string) {}
speak(): string {
return `${this.name} makes a noise.`
}
}
// 2. arguments object
// Before (JS)
function sum() {
return Array.from(arguments).reduce((a, b) => a + b, 0)
}
// After (TS)
function sum(...numbers: number[]): number {
return numbers.reduce((a, b) => a + b, 0)
}
// 3. Dynamic object access
// Before (JS)
function getValue(obj, key) {
return obj[key]
}
// After (TS)
function getValue<T, K extends keyof T>(obj: T, key: K): T[K] {
return obj[key]
}
// 4. Callback-based → Promise/async
// Before (JS)
function readFileCallback(path, callback) {
fs.readFile(path, 'utf-8', (err, data) => {
if (err) return callback(err)
callback(null, data)
})
}
// After (TS)
async function readFileAsync(path: string): Promise<string> {
return fs.promises.readFile(path, 'utf-8')
}
Third-Party Library Types
# Install @types packages
npm install --save-dev @types/node @types/express @types/lodash
# Handle libraries without types
# tsconfig.json
{
"compilerOptions": {
"typeRoots": ["./node_modules/@types", "./types"]
}
}
// types/legacy-lib.d.ts — declare types for untyped libraries
declare module 'legacy-library' {
export function doSomething(value: string): Promise<void>
export function compute(a: number, b: number): number
export const VERSION: string
}
// Simple case — module declaration only
declare module 'untyped-module' // Treated as any type
Pro Tips
Measure Migration Progress
# Count any occurrences (decreasing count = migration progress)
grep -r ": any" src --include="*.ts" | wc -l
# ts-migrate automated migration tool
npm install -g @ts-migrate/ts-migrate
ts-migrate-full /path/to/project
Automate Incremental Strictness
// Add rules to .eslintrc.json
{
"rules": {
"@typescript-eslint/no-explicit-any": "warn", // warn → later change to error
"@typescript-eslint/no-unsafe-assignment": "warn",
"@typescript-eslint/no-unsafe-call": "warn"
}
}