ncremental)** |
Key Findings:
- JSDoc pre-annotation reduces
any type proliferation by ~60% before .ts conversion.
- Incremental renaming of utility modules first establishes type boundaries with minimal cross-module impact.
- Phased strict mode activation correlates with a 7x reduction in post-migration runtime type errors compared to immediate enforcement.
Core Solution
The migration strategy relies on TypeScript's incremental adoption features, configured through deliberate tsconfig.json evolution and JSDoc-driven type injection.
Step 1: Enable allowJs & checkJs
Initialize TypeScript without breaking existing JavaScript execution. This allows the compiler to analyze .js files while preserving runtime behavior.
// tsconfig.json (Baseline)
{
"compilerOptions": {
"target": "ES2020",
"module": "ESNext",
"moduleResolution": "node",
"allowJs": true,
"checkJs": true,
"noEmit": true,
"strict": false,
"skipLibCheck": true,
"esModuleInterop": true
},
"include": ["src/**/*"],
"exclude": ["node_modules", "dist"]
}
Step 2: Inject JSDoc Type Annotations
Before renaming files, annotate critical interfaces, function signatures, and data shapes using JSDoc. This creates a type contract that TypeScript can validate during checkJs.
// src/utils/formatDate.js
/**
* @param {Date} date
* @param {string} [locale='en-US']
* @returns {string}
*/
export function formatDate(date, locale = 'en-US') {
return new Intl.DateTimeFormat(locale).format(date);
}
/**
* @typedef {Object} UserPayload
* @property {string} id
* @property {string} email
* @property {number} createdAt
*/
/**
* @param {UserPayload} payload
* @returns {Promise<boolean>}
*/
export async function syncUser(payload) {
// implementation
}
Step 3: Incremental .ts Renaming
Rename files in dependency order: utilities β shared hooks β business logic β UI components. Update import paths and resolve type mismatches immediately.
# Example migration script (bash)
find src -name "*.js" -type f | while read file; do
ts_file="${file%.js}.ts"
mv "$file" "$ts_file"
echo "Converted: $file -> $ts_file"
done
Step 4: Enable Strict Mode Phases
Activate strict compiler flags incrementally to avoid build paralysis. Each flag targets a specific class of type safety gaps.
// tsconfig.json (Strict Phase 1)
{
"compilerOptions": {
"strict": true,
"noImplicitAny": true,
"strictNullChecks": true,
"strictFunctionTypes": true,
"strictBindCallApply": true,
"strictPropertyInitialization": true,
"noImplicitThis": true,
"alwaysStrict": true
}
}
Architecture Decisions:
- Dual Compilation Strategy: Maintain
allowJs: true until type coverage exceeds 85%, then disable it to enforce .ts-only development.
- Module Boundary Enforcement: Use
isolatedModules: true to ensure each file can be safely transpiled independently, preventing cross-file type leakage during incremental builds.
- CI Gate Integration: Run
tsc --noEmit as a blocking step in pull requests to prevent type regression during active migration.
Pitfall Guide
- Premature Strict Mode Activation: Enabling
strict: true before resolving any types and nullability gaps causes immediate build failures. Developers resort to @ts-ignore or any, creating technical debt that compounds. Best Practice: Enable flags individually (noImplicitAny β strictNullChecks β strict) and resolve violations per module.
- JSDoc Syntax Misalignment: Using incorrect JSDoc tags (e.g.,
@type on variables vs @param on functions) or mismatched union syntax breaks checkJs validation. Best Practice: Validate JSDoc with tsc --checkJs early and use IDE linting rules to enforce correct closure-style annotations.
- Module System Conflicts: Mixing CommonJS (
require/module.exports) and ES Modules (import/export) during transition causes runtime resolution errors and bundler warnings. Best Practice: Standardize on ES modules before renaming. Use tsconfig.json "module": "ESNext" and update bundler configs to handle .ts extensions explicitly.
- Ignoring Build Pipeline Updates: Failing to update Webpack, Vite, or Rollup configurations to prioritize
.ts resolution leads to silent fallbacks to .js files or duplicate module instances. Best Practice: Update resolve.extensions to ['.ts', '.tsx', '.js', '.jsx'] and ensure transpilation rules exclude .ts from Babel/legacy loaders.
- Over-Annotating Inferred Types: Adding explicit type hints where TypeScript's control flow analysis already infers types correctly slows migration without improving safety. Best Practice: Only annotate function boundaries, external APIs, and complex data shapes. Rely on inference for internal variables and control flow.
- Skipping Incremental Testing: Migrating without corresponding unit/integration test coverage allows type changes to silently alter runtime behavior. Best Practice: Pair each
.js β .ts conversion with test execution. Use jest --coverage to verify behavioral parity before committing.
Deliverables
- π Migration Blueprint: A 4-phase roadmap (Week 1β4) detailing dependency mapping, JSDoc injection targets, strict flag rollout sequence, and CI/CD integration checkpoints.
- β
Pre-Migration Checklist: 28-point validation covering dependency audit, bundler configuration, test baseline establishment, JSDoc syntax verification, and team type-safety training requirements.
- βοΈ Configuration Templates: Production-ready
tsconfig.json variants (Baseline, JSDoc-Active, Strict-Phase 1, Strict-Phase 2), ESLint @typescript-eslint ruleset, and Prettier formatting overrides for mixed JS/TS environments.