skip to content

What does TypeScript's `isolatedModules` compiler flag do, and does enabling it change the JavaScript that `tsc` emits?

level: middleimportance: must knowfreq 57%

answer

  1. about portability, not output
  2. one file at a time
  3. adds errors, changes nothing emitted
  4. barrel re-exports of types are the usual casualty
  5. ambient const enums and non-module files too

basics

~20 s

isolatedModules changes no output at all. It is a checking-only flag: it makes the compiler report source constructs that cannot be compiled correctly by a tool that sees one file at a time, so the code stays portable to per-file transpilers.

solid answer

~50 s

`isolatedModules` adds errors; it never changes emit. Turning it on tells `tsc` to reject any construct whose correct translation would require information from another file, so the source stays safe for tools that transpile file by file — esbuild, swc, Babel's TypeScript transform. The classic case is re-exporting a type: `export { User } from "./models"` is fine for `tsc`, which knows `User` is an interface and elides it, but a single-file tool cannot know that and emits a re-export of something that does not exist at runtime. Under the flag that becomes an error and you write `export type { User } from "./models"`. It also rejects referencing ambient `const enum` members, and flags a `.ts` file with no top-level import or export, since per-file tools treat every file as a module — adding `export {}` fixes that one.

go deeper

for a junior

Know that this flag adds compile errors rather than changing output, and that its purpose is keeping each file compilable on its own. Recognising the export type fix on a barrel file is enough at this level.

for a middle

Explain why per-file tools cannot decide whether an imported name is a type, and name concrete rejected constructs: plain type re-exports, ambient const enum references, and files that are not modules. Say plainly that emit is unchanged.

for a senior

Frame it as converting build-tool-only runtime failures into compile errors, and describe adopting it on a legacy codebase: expect the errors to cluster in barrel files, and know each fix is mechanical and syntactic.

for a principal

Own the decision to enable it before any transpiler swap is on the table. Argue the cost — a one-off mechanical migration — against keeping the option of a faster build, and set the convention so new code cannot reintroduce cross-file-dependent constructs.

## The contract `tsc` compiles a *program*: it reads every file, builds a full type graph, and only then emits. That whole-program knowledge leaks into its output decisions. Import elision is the clearest example — the compiler drops an import because it knows the imported name is an interface, and it knows that because it read the other file. A growing share of real builds do not work that way. esbuild, swc and Babel's TypeScript transform are **per-file transpilers**: they parse one file, strip the types syntactically, and emit JavaScript, never resolving an import. They are enormously faster precisely because they skip the type graph. But that means any TypeScript construct whose correct translation depends on another file is undecidable for them. `isolatedModules` is the flag that states the contract: *every file in this project must be compilable on its own*. It is worth being precise about what it does and does not do. **It does not change emit.** Byte-for-byte, `tsc` produces the same JavaScript with the flag on or off. It also does not make `tsc` compile files independently — `tsc` still type-checks the whole program. The flag only adds diagnostics. **It does not type-check your build tool's output.** It merely guarantees your *source* is free of constructs that would trip such a tool. ## What it rejects, and why **Re-exporting a type without the `type` modifier.** ```ts // index.ts export { User } from "./models"; // User is an interface ``` `tsc` sees the interface and emits nothing for it. A per-file tool sees only this text: `User` might be a function, so it must emit a real re-export — and the emitted `./models.js` has no `User`, so the module fails to link. Under `isolatedModules` this is an error, and the fix is syntactic: ```ts export type { User } from "./models"; // or, when mixing: export { type User, createUser } from "./models"; ``` **Referencing an ambient `const enum`.** A `declare const enum` has no emitted object anywhere; `tsc` inlines its member values from the declaration file. A tool that never reads that file has nothing to inline and nothing to reference, so the flag rejects such references outright. **A file that is not a module.** A `.ts` file with no top-level `import` or `export` is a *global script* in TypeScript's model — its declarations join the global scope. Per-file transpilers have no such notion; they emit a module. `isolatedModules` reports these files, and the conventional fix is a lone `export {};` at the bottom to make the file a module. ## Why this is a strictness flag, not a build flag The failures it prevents share a nasty property: they do not fail at compile time in the fast build, and they do not fail in the `tsc` build at all. They fail *at runtime, only in the fast build*, often only in production, as a missing export or an undefined value. The flag converts an entire class of build-tool-specific runtime bugs into ordinary compile errors that any contributor sees immediately. This is also why it is worth enabling even in projects that compile with `tsc` today. It costs nothing, it is enforced by the compiler you already run, and it keeps the door open to swapping in a faster transpiler later without a hunt for latent breakage. ## Adopting it On an existing codebase, expect a burst of errors concentrated in barrel files — index files full of `export { ... } from` lines are exactly where type re-exports accumulate. The fixes are mechanical: add `type` to the type-only specifiers, add `export {}` to the handful of script-shaped files, and replace ambient `const enum` usage with a regular enum or a union of literals. A useful mental test when you are unsure whether some construct will trip the flag: *cover the rest of the project and look only at this file — could you emit correct JavaScript for it?* If the honest answer needs a peek at another file, the flag will complain, and it is right to.

  • Does isolatedModules make tsc compile each file independently?
    No. `tsc` still reads and type-checks the whole program exactly as before, and emits identical output. The flag only reports source constructs that a genuinely per-file tool could not translate correctly, so it is a portability guarantee about your source rather than a change to how the compiler runs.
  • How does isolatedModules relate to verbatimModuleSyntax?
    They are complementary. `isolatedModules` is checking-only and asks whether a per-file tool could handle your source; `verbatimModuleSyntax` changes what `tsc` itself emits by removing import elision, which forces the same explicit `type` markers as a side effect. Enabling the second makes many of the first's errors unreachable, but they cover different ground.
  • Why is a .ts file with no imports or exports an error under isolatedModules?
    Because such a file is a global script in TypeScript's model, and its top-level declarations join the global scope. A per-file transpiler has no concept of a script versus a module and will emit a module regardless, changing the scoping. Adding a lone `export {};` makes the file a module and clears the error.

saying these in an interview costs you the question

  • Thinks isolatedModules changes or speeds up tsc's emitted output
  • Believes it makes tsc skip the whole-program type check
  • Assumes it type-checks whatever esbuild or swc produces
  • Cannot name a single construct the flag actually rejects
  • Says the flag is only needed if you already use Babel

context