skip to content

Incremental Migration Strategy

You will learn a staged path from JavaScript to strict TypeScript: allowJs first, checkJs file by file, rename leaf modules before hubs, then ratchet strict flags on one at a time instead of all at once. Interviewers ask this as a judgment question — they want a plan with a rollback story, not a heroic weekend rewrite.

part ofTypeScriptoverview, primer and where to startread it →
on this pageshow

questions

5

In a project already compiled by tsc with allowJs enabled, you rename utils.js to utils.ts and change nothing inside the file. Does the JavaScript that ships behave any differently, and what does change?

level: juniorimportance: should knowfreq 40%

answer

  1. compile-time layer only
  2. nothing left after emit
  3. the checker's opinion, not the output
  4. JSDoc types only read in .js
  5. implicit any surfaces on rename

basics

~20 s

Behaviour does not change: TypeScript erases types, and a freshly renamed file has none to erase, so tsc emits the same JavaScript. What changes is checking — the file is now checked as TypeScript, so implicit anys and unsound patterns start appearing as errors.

solid answer

~40 s

Renaming is a checking change, not a runtime change. TypeScript's type layer is erased at emit, so a file that carries no annotations compiles to the same output it did as JavaScript — and under `allowJs` it was already going through tsc, so even the emit settings are identical. What flips is how strictly the file is read: it is now a TypeScript source, so unannotated parameters trigger `noImplicitAny`, inference is tighter, and real errors surface. Three specific things stop working: JSDoc type comments are only consulted in `.js` files, so any types you had expressed that way are dropped; tsc recognises CommonJS export patterns like `module.exports = x` only in JavaScript files, so those must become `export` declarations; and a file containing JSX must become `.tsx`, not `.ts`.

go deeper

for a junior

Be ready to say plainly that types are erased, so renaming a file does not change what runs — it changes what the compiler will complain about. Mention that JSX files need the .tsx extension.

for a middle

Explain why the emit is unchanged under allowJs, and name the concrete things that stop working: JSDoc types are read only in .js files, and CommonJS export patterns are recognised only there too.

for a senior

Show migration discipline: rename in small revertable batches, prefer explicit any over implicit as a trackable marker, and call out the one real risk — a file that was never passing through tsc now gets downleveled to your target and module settings.

for a principal

Own the framing that renames are cheap and the follow-up edits are where the risk lives. Set the policy for how much redesign is allowed inside a migration commit and how the team tracks remaining any debt across the codebase.

## The rule underneath the answer TypeScript is a static type layer over JavaScript. Everything the type system knows is used during compilation and then thrown away: the emitted file contains no type checks, no reflection data, and no runtime cost from annotations. That is why a rename is such a cheap migration step — you are changing which rules the *checker* applies, not what the program does. The practical consequence for a migration is that renaming files is reversible and low risk. The dangerous parts of a migration are the changes you make *after* the rename to satisfy the checker; the rename itself is close to free. ## What the compiler was already doing With `allowJs` turned on, `.js` files are part of the compilation. tsc parses them, infers types for their exports, applies your `target` and `module` settings, and emits output. So before the rename, this file was already tsc's responsibility. After the rename the same text is parsed under TypeScript rules and emitted under the same options — which is why the output is effectively unchanged. The important exception: if the file was **not** previously going through tsc at all (no `allowJs`, or the file was excluded and copied by some other step), then after the rename it starts being transformed — downleveled to your `target`, and its imports/exports rewritten to your `module` format. That is a real behaviour surface, and it is the one case where "just a rename" deserves a test run. ## What actually changes **1. Errors appear.** The file is now checked. Parameters without annotations report `Parameter 'x' implicitly has an 'any' type` under `noImplicitAny`; property access on values the checker cannot model is flagged; and if `strictNullChecks` is on, every place a value might be `null` or `undefined` becomes visible. None of this changes the emitted code — tsc emits by default even when it reports errors — but it is now blocking in CI. **2. JSDoc types stop counting.** TypeScript reads JSDoc type annotations such as `@type`, `@param` and `@returns` only in JavaScript files. In a `.ts` file they are documentation, nothing more: ```ts /** @param {string} name */ export function greet(name) { // in a .ts file this parameter is an implicit any, // not a string — the JSDoc is ignored for typing return `hi ${name}`; } ``` If the file was already JSDoc-typed, the rename is where you move those types into TypeScript syntax: `export function greet(name: string)`. **3. CommonJS export patterns stop being recognised.** In a JavaScript file, tsc understands `module.exports = something` and `exports.foo = something` as the module's exports. In a TypeScript file it does not treat them that way — you write `export` declarations (or `export =` when you genuinely need the single-value CommonJS shape). **4. JSX needs `.tsx`.** A file containing JSX must be renamed to `.tsx`, and in `.tsx` the angle-bracket assertion form `<Foo>value` is ambiguous with JSX syntax, so assertions there use `value as Foo`. ## How to do the rename well Rename in small, self-contained commits — ideally one file, or one small directory, per pull request. Resist the urge to redesign types while you are at it: let inference do the work, annotate only what the checker complains about, and where you genuinely cannot type something yet, write an explicit `any` rather than leaving it implicit. An explicit `any` is a grep-able marker of remaining debt; an implicit one is invisible. Run the test suite after the batch. Not because the rename changes behaviour, but because the *edits you made to satisfy the checker* can — reordering an initialiser, adding a guard, changing an export shape. ## What the rename does not buy you Nothing about the rename adds runtime safety. Data arriving from a network response, `JSON.parse`, `localStorage` or a form is still whatever the runtime hands you; annotating it as `User` is a claim the compiler trusts, not a check it performs. Validation at those boundaries remains a runtime job, and a migration does not change that.

  • Does the rename mean you have to annotate every parameter and return type right away?
    No. Inference covers most local code — variable initialisers, return types, and anything flowing from typed calls. You generally only annotate what the checker cannot infer: function parameters, and values crossing a module or I/O boundary. Where the right type is genuinely unclear, write an explicit `any` and move on; it is a marker you can search for later, unlike an implicit one.
  • The file ends with `module.exports = { parse };`. What has to change after renaming it to .ts?
    Rewrite it as a TypeScript export: `export { parse }` in a normal module, or `export = { parse }` if consumers genuinely depend on the single-value CommonJS shape. tsc infers exports from `module.exports` assignments only in JavaScript files, so in a `.ts` file that line stops declaring anything — the module looks empty to importers, and `module` itself is only in scope if Node's types are installed.
  • What about a JavaScript file that contains JSX?
    It becomes `.tsx`, not `.ts`, and the `jsx` compiler option has to be set for the file to compile. One syntax difference follows: in `.tsx` the angle-bracket assertion `<Foo>value` is ambiguous with a JSX element, so assertions must be written `value as Foo`.

saying these in an interview costs you the question

  • Thinks renaming to .ts adds runtime type checking
  • Expects the emitted JavaScript to change shape
  • Believes JSDoc @type keeps typing the file after rename
  • Assumes every parameter needs an annotation immediately
  • Renames a file containing JSX to .ts

context

open as a page

When migrating a JavaScript codebase to TypeScript file by file with tsc, why is it usually better to convert leaf modules — the ones that import little or nothing else from the project — before converting the hub modules that most of the codebase imports?

level: middleimportance: should knowfreq 50%

basics

~20 s

Because a converted file's types are only as good as its dependencies. Converting leaves first means every new TypeScript file rests on already-typed code, so inference produces real types instead of weak ones you must revisit, and each change stays small and revertable.

open as a page

During a TypeScript migration, tsc reports errors that originate inside .d.ts files under node_modules. What does setting skipLibCheck to true in tsconfig.json actually skip, and what do you give up by leaving it on?

level: middleimportance: should knowfreq 45%

basics

~20 s

skipLibCheck stops tsc type-checking the contents of every declaration file — dependencies' and your own alike, including hand-written shims. Your code is still checked against those declarations, so what you give up is any warning that a declaration file is itself wrong or inconsistent.

open as a page

A half-migrated codebase sets "strict": true in tsconfig.json and tsc reports several thousand errors. How would you get the project to full strict without freezing feature work?

level: seniorimportance: should knowfreq 55%

basics

~20 s

Do not merge the all-at-once change. Stage it: keep strict on but switch individual flags back off and re-enable them one at a time, or scope strict to migrated directories, and make CI hold a baseline error count that is only ever allowed to fall.

open as a page

You own a large, actively developed JavaScript codebase and the team wants type safety. How would you decide between an incremental TypeScript migration, a rewrite in TypeScript, and staying on JavaScript with types checked by tsc?

level: principalimportance: should knowfreq 35%

basics

~20 s

Incremental is the default because the codebase stays shippable and every step is revertable. A rewrite is only defensible for a small, stable, well-covered module. Staying on typed JavaScript is the right answer when the build cannot change or the code ships as source.

open as a page