learn
TypeScript

Migrating a JavaScript project

AllowJs, checkJs, JSDoc and a five-step path that never stalls.

Nobody rewrites a working app in a weekend. Migration is a ratchet: check the JavaScript you have, annotate the seams, rename files one by one, and tighten strictness last. Interviewers ask for the order — recite these five steps.

Real-life situation

A checkout helper has grown for two years with no types. Step one flips on checkJs so the compiler reads the existing JavaScript. Step two adds JSDoc annotations to the trickiest function. Steps three and four rename one file to .ts and fix only its errors. Step five raises strictness. Each step ships; the build never goes red for a week.

Visual

Migration path
BrowserAPIRealtimeCacheDatabaseShareddurableephemeral

Try it

A half-migrated loader still fails

Checked with TypeScript 6.0.3, strict mode

typescriptTab moves to the next control
async function loadUserIds(): Promise<number[]> {
try {
  const response = await fakeFetch("https://api.example.com/users");
  const users = (await response.json()) as Array<{ id: number }>;
  return users.map(function (user) {
    return user.id;
  });
} catch (e) {
  console.log("failed: " + e.message);
  return [];
}
}
loadUserIds().then(function (ids) {
console.log("ids: " + ids.join(","));
});

Compiler not loaded. Check or Run loads it here (about 9 MB) — nothing downloads until you do.

Runner helpers

sleep(ms) waits; fakeFetch(url) returns deterministic sample data. TypeScript is stripped before running; imports and exports are not available in the sandbox.

Try narrowing the catch variable — e instanceof Error ? e.message : "unknown" — and the Types tab goes clean. That is step four in miniature: one file, one error species, fixed and shipped. allowJs lets .js files stay in the build during the transition, checkJs types them loosely, and JSDoc (@param {number[]} prices) annotates without renaming anything.

Will it compile? — decide first, then reveal.

check.ts
let nickname: string;
console.log(nickname);
nickname = "sam";

Interview questions

What is a safe first step when migrating JavaScript to TypeScript?basic

Rename one small, well-tested file from .js to .ts and fix only its errors before moving on. Start at the leaves — utility modules with few imports — so each conversion stays reviewable. Keep the rest of the app running in JavaScript throughout. Small vertical slices beat a big-bang rewrite: every commit stays shippable and each fix teaches the patterns the next file will need.

Interviewers also ask

  • Which files should you convert first?
How do allowJs and checkJs enable gradual migration?intermediate

allowJs lets tsc compile your existing .js alongside new .ts, so the project builds during the transition. Adding checkJs extends type checking into those JavaScript files, surfacing errors without renaming anything yet. You tighten file by file, converting the cleanest ones first. Once a file is fully typed, rename it to .ts and let the stricter rules apply. It is the official on-ramp for large codebases.

Interviewers also ask

  • When do you rename a file from .js to .ts?
How does JSDoc help during a gradual migration?intermediate

JSDoc comments like @param {string} name give JavaScript files real types that checkJs enforces, without changing any runtime code. You document function contracts in place, fix the errors they reveal, and later convert each annotation mechanically into TypeScript syntax. It suits teams that must keep shipping .js while still gaining safety. Treat dense JSDoc as a staging area — convert to .ts once a file's annotations stabilize.

Interviewers also ask

  • How does a JSDoc type become a TypeScript annotation?
How should you handle the flood of implicit any errors during migration?basic

Work through them top-down from shared types outward: define the core domain interfaces first, then annotate function boundaries to use them. Prefer real types over quick any fixes — each honest annotation resolves several downstream errors at once. Where the shape is genuinely unknown, use unknown plus a guard instead of any. Track remaining errors as a visible count so the team sees steady progress toward zero.

Interviewers also ask

  • Why fix shared types before leaf functions?
How do you keep tests passing while converting to TypeScript?intermediate

Convert source files before test files, keeping the runtime behavior identical — types alone should not change what the code does. Run the existing suite after each file conversion so regressions point at the last change. Only then add type-aware assertions for the new contracts. If a test needs loosening with a cast during transition, leave a comment naming the follow-up. Green tests on every commit prove the migration is behavior preserving.

Interviewers also ask

  • Should behavior change during a type migration?
When should you enable strict mode during a migration?intermediate

Finish a lenient pass first — whole codebase compiling with basic checking — then enable strict flags one at a time, starting with strictNullChecks. Each flag produces a focused batch of real bugs to fix and review separately. Turning everything on at once buries the team in unrelated errors and stalls the migration. Celebrate each flag going green: it marks a permanent safety upgrade, not just a config tweak.

Interviewers also ask

  • Which strict flag should you enable first?

Key takeaways

  • Check first (checkJs), annotate second (JSDoc), rename third, strict last.
  • Fix one error species at a time; every step must ship green.
  • allowJs keeps old files building while new files get full checking.

References

On this page