skip to content

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