skip to content

isolatedModules & verbatimModuleSyntax

You will learn the rules that let a tool compile each file on its own with no cross-file type information: why type-only imports must be marked explicitly, why re-exporting a type can break, and what happens to const enums and namespaces. Interviewers ask because every esbuild, swc, or Babel pipeline depends on this contract holding.

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

questions

5

In TypeScript, what does `import type { User } from './models'` mean, and how does it differ from a plain `import { User } from './models'`?

level: juniorimportance: must knowfreq 68%

answer

  1. about what survives compilation
  2. a promise, not an optimisation
  3. type positions only, never a value
  4. guarantees erasure; plain imports rely on elision
  5. inline form marks single specifiers

basics

~20 s

import type declares the binding as types only: TypeScript always erases that statement from the emitted JavaScript, and the name cannot be used as a value. A plain import is erased only when the compiler decides nothing in the file uses it at runtime.

solid answer

~50 s

`import type` is an explicit promise to the compiler that everything the statement brings in is used only in type positions. Two things follow. First, the statement is **guaranteed** to be erased — nothing is emitted for it, so it cannot even trigger the imported module's side effects. Second, the binding is usable only where a type is expected: writing `new User()` or passing `User` as a value is a compile error. A plain `import { User }` is different in kind: TypeScript's *import elision* will drop it from the output if it sees the name used only as a type, but that decision depends on whole-program information, and if you later use `User` as a value the import quietly reappears in the emitted JavaScript. There is also an inline form, `import { type User, createUser } from './models'`, which marks individual specifiers when one statement carries both types and values.

go deeper

for a junior

Know that import type brings in only a type, never a runtime value, and that the whole statement vanishes from the compiled JavaScript. Be able to say what breaks if you then try to call or construct that binding.

for a middle

Explain import elision as the implicit alternative, and why an explicit type keyword is decidable from a single file while elision needs whole-program information. Show the inline type specifier form and the matching export type syntax.

for a senior

Show the runtime failure mode: a type-only import left unmarked survives a per-file transpiler and produces a module that requests an export nobody emitted. Be able to talk about relying on side-effect imports separately from type imports.

for a principal

Own the convention. Decide whether the codebase writes type-only imports by hand and has the compiler enforce it, weighing a one-time mechanical migration against the class of build-tool-only runtime failures it retires for every future contributor.

## The problem this syntax solves TypeScript's central rule is that types are erased: the compiler emits JavaScript with the type layer removed. That creates an awkward case for imports. `import { User } from './models'` is ordinary JavaScript syntax that, at runtime, actually loads `./models` and binds one of its exports. But if `User` is an interface, there is no runtime export named `User` at all — the interface emitted nothing. Keeping that import in the output would produce a module that fails to link. TypeScript's historical answer is **import elision**: while emitting, the compiler looks at how each imported name is used and drops the ones that are only ever used in type positions, dropping the whole statement when nothing is left. Elision is convenient but implicit, and its correctness depends on information from other files. ## What the explicit form does ```ts import type { User } from "./models"; ``` This declares up front that the statement is type-only. Two guarantees follow. **It is always erased.** The emitted JavaScript contains no trace of the statement, so the module is never loaded because of it. If `./models` had a top-level side effect you were relying on, a type-only import will not run it — you would need a separate `import "./models";`. **The binding is a type, not a value.** Everything below is legal, because each use is a type position: ```ts let current: User; function save(u: User): Promise<void> { /* ... */ } interface Session { user: User } ``` And this is a compile error, because it uses the name where a runtime value is required: ```ts const u = new User(); // error: 'User' cannot be used as a value console.log(User); // error ``` That second guarantee is why the form is useful even for classes. A class produces *both* a type and a runtime value; importing it with `import type` deliberately takes only the type half, and the compiler will stop you the moment someone reaches for the value half. ## The inline modifier When one module gives you both types and values, you do not need two statements: ```ts import { type User, createUser } from "./models"; ``` `createUser` is a real value import and survives into the output; `type User` is dropped. There is a matching export form: ```ts export type { User }; export { type User, createUser } from "./models"; ``` ## Why interviewers care Elision requires the compiler to know what every imported name *is*, which requires reading the other file. Tools that compile one file at a time — esbuild, swc, Babel's TypeScript transform — never read the other file. They see `import { User } from './models'` and have no way to tell an interface from a function, so they must keep the import. If `User` was type-only, the emitted module now imports something that does not exist. Explicit `import type` removes the guesswork: the `type` keyword is *syntax*, visible in the single file being transpiled, so any tool can delete it correctly without cross-file knowledge. That is why compiler options exist to require it, and why modern codebases write it by hand rather than relying on elision. ## Practical guidance - Reach for `import type` (or the inline `type` modifier) whenever an import is genuinely type-only. It costs nothing and documents intent. - Do not use it for a module you import *for* its side effect; use a bare `import "./setup";` for that. - Remember the direction of the guarantee: `import type` guarantees erasure, whereas a plain import only *might* be erased. When in doubt, being explicit is always the safe side. - The same reasoning applies to exports: re-exporting a type through a plain `export { ... } from` is the mirror-image hazard, and `export type { ... } from` is the mirror-image fix.

  • If a module needs to be loaded for its side effects and you only use a type from it, what do you write?
    Two statements: `import type { Handler } from "./plugin";` for the type, plus a bare `import "./plugin";` to keep the side effect. A type-only import emits nothing at all, so on its own it will never load the module and the side effect silently disappears.
  • A class is both a type and a value. What breaks if you import it with `import type`?
    Only the value half. Annotations, `implements` clauses and `extends` in an interface still work, but `new Klass()`, `Klass.staticMethod()`, `instanceof Klass` and passing `Klass` as an argument all become compile errors, because the binding was declared type-only and is erased from the output.
  • Why does the inline form `import { type A, b }` exist when `import type { A }` already does?
    So a single module's types and values can travel in one statement instead of two. The statement is kept in the emitted JavaScript for `b`, with the `type A` specifier removed. It also lets a tool that reads only this file decide correctly, since the marker is right there in the syntax.

saying these in an interview costs you the question

  • Calls import type a style preference with no compile-time effect
  • Says import type makes the import faster or smaller at runtime
  • Expects a type-only import to still run the module's side effects
  • Uses an import type binding in a new expression or instanceof check
  • Assumes a plain import of an interface always disappears from the output

context

open as a page

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

level: middleimportance: must knowfreq 57%

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.

open as a page

Why does TypeScript's `isolatedModules` flag reject references to an ambient `declare const enum`, and what does a per-file transpiler do with an ordinary `const enum`?

level: middleimportance: should knowfreq 30%

basics

~20 s

A const enum is normally compiled away by inlining its member values, which requires reading the declaring file. An ambient const enum emits no runtime object at all, so a tool that reads one file has nothing to inline and nothing to reference — hence the error.

open as a page

What does TypeScript's `verbatimModuleSyntax` flag change about how imports and exports are emitted, and which older options does it replace?

level: middleimportance: should knowfreq 44%

basics

~10 s

verbatimModuleSyntax replaces import elision with one syntactic rule: anything written with a type modifier is dropped entirely, and everything else is emitted exactly as written. Added in TypeScript 5.0, it supersedes importsNotUsedAsValues and preserveValueImports.

open as a page

A TypeScript package's index.ts contains `export { Config } from './config';` where Config is an interface. Built with `tsc` the package works, but built by a per-file transpiler such as esbuild or swc, consumers crash with an error saying './config' provides no export named 'Config'. Why does the same source behave differently, and how do you fix it?

level: seniorimportance: should knowfreq 38%

basics

~20 s

tsc reads ./config, sees Config is an interface, and drops it from the re-export. A per-file transpiler never reads that file, so it emits a real re-export of a binding that the emitted config.js does not have. Fix it with export type, and enable isolatedModules so the compiler catches every such site.

open as a page