Migrate JavaScript to TypeScript
Adopting TypeScript does not require a rewrite. Turn on allowJs/checkJs, add a tsconfig.json with a gentle strictness ramp, and rename files .js to .ts one module at a time while the build stays green.
Last verified · Updated May 22, 2026
Adopting TypeScript is incremental, not a rewrite. Enable allowJs and checkJs to type-check existing JavaScript, add a tsconfig.json with a loose-to-strict ramp, then rename files .js to .ts module by module while the build stays green.
The incremental adoption model
TypeScript is designed to coexist with JavaScript. Start by adding a tsconfig.json with allowJs so .js and .ts files build together. Turn on checkJs to surface type problems in JavaScript without renaming anything. Then migrate the highest-value modules first — rename .js to .ts, add types, and repeat. You never need a big-bang cutover.
Step-by-step adoption
- Install typescript and the @types packages for your runtime and key dependencies.
- Add a tsconfig.json with allowJs: true and a loose baseline (strict: false).
- Enable checkJs (or per-file // @ts-check) to surface issues in existing JS.
- Rename leaf modules .js to .ts first, then work up the dependency graph.
- Tighten strictness one flag at a time: noImplicitAny, then strictNullChecks, then full strict.
Starter tsconfig.json
{
"compilerOptions": {
"allowJs": true,
"checkJs": false,
"strict": false,
"noImplicitAny": false,
"moduleResolution": "bundler",
"skipLibCheck": true,
"outDir": "dist"
},
"include": ["src"]
}When a line genuinely cannot be typed yet, prefer // @ts-expect-error: it errors if the underlying problem is later fixed, so suppressions self-clean instead of rotting. Avoid // @ts-ignore, which hides errors silently and forever.
Handling untyped dependencies
- Install @types/<pkg> from DefinitelyTyped when available.
- For libraries without types, add a declarations.d.ts with `declare module "pkg";` to unblock imports.
- Set skipLibCheck: true so third-party .d.ts errors don't block your own migration.
- Run `npm why @types/node` to catch duplicate or mismatched type packages.
Adoption checklist
- tsconfig.json committed with allowJs and a loose baseline
- typescript and @types/* installed and deduped
- CI runs `tsc --noEmit` and gates new errors only
- A strictness ramp is agreed (noImplicitAny → strictNullChecks → strict)
Related paths
Official sources
Backs the breaking-change and migration-step claims.
Backs the breaking-change and migration-step claims.
Copy-ready AI prompts
Structured prompts for an AI coding assistant. Inspect first, then execute incrementally, and keep a human in the review loop.
You are helping with a TypeScript migration: incremental JavaScript-to-TypeScript adoption. Do not edit files yet. First inspect the repository and report: 1. The exact typescript version in package.json and the lockfile, plus every @types/* package and whether duplicates resolve to different versions (run `npm ls typescript` and `npm why @types/node`). 2. The active tsconfig.json options: strict flags, moduleResolution, target/module, experimentalDecorators, isolatedModules, skipLibCheck, and any removed-in-5.0 flags (--out, --target ES3, --keyofStringsOnly, --charset, --noImplicitUseStrict). 3. Files using decorators and whether they rely on the legacy (experimentalDecorators) model. 4. The build/typecheck command and whether the project emits with tsc or a bundler. 5. Any // @ts-ignore / // @ts-expect-error suppressions and any JS files type-checked via allowJs/checkJs. Return: a risk summary, the highest-risk files, a suggested migration order, the commands to run before editing, and any questions that need human confirmation.
Safety: Inspection only. The agent must not modify files in this step.
Works with Claude Code, Cursor, GitHub Copilot
Perform the migration (incremental JavaScript-to-TypeScript adoption) one concern at a time. Work in this order and pause for review after each: (1) bump the typescript devDependency and dedupe @types/* packages, (2) update tsconfig.json — remove deleted flags and set moduleResolution to bundler or node16 as appropriate, (3) run `tsc --noEmit` and triage errors by code, (4) fix type errors in small batches, (5) reconcile decorators to a single model. After each step run `tsc --noEmit` and the project's test command, and report the error count and results before continuing. Do not refactor unrelated code or loosen strictness to make errors disappear.
Safety: Apply changes incrementally and keep each step reviewable. Never silence errors with broad any or // @ts-ignore to force a green build.
Works with Claude Code, Cursor, GitHub Copilot
Test plan
Commands
npx tsc --noEmitnpm run lintnpm test -- --watch=falsenpm run build
Manual checks
- Decorators: exercise any class using decorators (DI, ORM entities, validators) at runtime.
- Emit: confirm the compiled output and declaration (.d.ts) files match the previous shape.
- Module resolution: verify imports of ESM/CJS dependencies still resolve at runtime.
Regression risks
- Mixed legacy and TC39 decorators in the same project.
- skipLibCheck masking type errors inside dependency .d.ts files.
- Duplicate @types/* packages resolving to conflicting versions.
Acceptance criteria
- `tsc --noEmit` passes with zero errors on the target version.
- No new // @ts-ignore or // @ts-expect-error were added to mask real errors.
- Build and test suites pass on the upgraded toolchain.
Frequently asked questions
Do I have to convert the whole codebase at once?
No. allowJs lets .js and .ts coexist, so you migrate module by module. Many teams run mixed for months and only tighten strictness once the bulk of the code is typed.
Should I enable strict mode immediately?
Usually not. Start with strict: false and ramp up — noImplicitAny first, then strictNullChecks, then full strict. Enabling everything on day one buries the team in errors and stalls adoption.