TypeScript doesn’t run your code, but your bundler might think it does. Learn why marking your type imports with import type (and enabling `verbatimModuleSyntax`) makes your builds faster, cleaner, and more predictable.
Every modern JavaScript app today (React, Next.js, SvelteKit, you name it) starts with TypeScript.
But here's the thing: TypeScript doesn't actually run.
It's easy to forget this. You write code, import modules, and your IDE provides autocomplete; everything feels alive and connected. But at build time, all that type information disappears. What's left is just plain JavaScript. And yet, sometimes, your bundler still behaves as if those types exist.
That's how you might end up with bundlers unnecessarily trying to resolve or include files that were never supposed to exist: sometimes resulting in confusing behavior or extra work during the build, even if it doesn't always cause visible errors.
The invisible boundary between types and runtime
To understand why this happens, you need to follow what actually occurs during a build.
Let's take a typical setup: a TypeScript app running through Vite or Webpack.
TypeScript phase: The compiler parses your
.tsfiles, checks types, and then erases them.Emit phase: It outputs
.jsfiles with only the executable code.Bundler phase: Vite or Webpack scans that JavaScript to build a dependency graph.
Runtime phase: The browser (or Node) runs that code.
Somewhere between steps 1 and 3, something subtle happens:
TypeScript sees your imports and has to decide whether each one represents real runtime code or type-only information.
If it guesses incorrectly, the bundler might attempt to follow a file path that only exists in your type system, creating unnecessary dependencies or slowdowns during the build.
A simple example of a ghost import
Here's a tiny function that looks perfectly harmless:
You'd expect this to compile down to something clean like:
But that's not what happens.
By default, TypeScript will keep the import statement in the emitted JavaScript:
Now your bundler thinks there's a real file called types.js to include.
It won't necessarily fail, but it can introduce extra work or confusion in the dependency graph. This is what I call a ghost import: something that only existed in the type world but managed to haunt your runtime.
Why TypeScript can't always guess correctly
For years, the TypeScript compiler tried to be smart about this. It attempted to elide imports that were only used for types.
But that guessing logic was fragile. It depended on subtle rules:
Whether an imported symbol was used in a value position.
Whether it appeared inside
typeof,extends, or generic parameters.Whether your build target used isolated modules.
And it didn't always work consistently—especially when mixed with frameworks that do their own module analysis (like Next.js or Astro).
That's why TypeScript 5.0 introduced a stricter, saner option:
🖐 verbatimModuleSyntax
The fix: make your imports honest
When you enable this compiler flag, TypeScript stops guessing altogether.
It expects you to tell it which imports are types and which ones are real.
In your tsconfig.json:
Once you turn it on, this no longer compiles silently:
You have to be explicit:
Now, the compiler recognizes that this import is type-only and removes it entirely from the emitted JavaScript.
No more ghost modules. No more bundler confusion. Just clean runtime code that matches what actually executes.
Why this flag exists (and why it matters)
This change isn't cosmetic. It's about correctness.
TypeScript's type system lives in a different universe than your JavaScript runtime. Mixing the two without clear boundaries leads to subtle build issues and confusing mental models.
Enabling verbatimModuleSyntax enforces that separation. It means:
Your runtime reflects reality — if it’s not executed, it’s not imported.
Your builds are faster — fewer files to resolve, fewer modules to traverse.
Your integrations are safer — tools like Babel, SWC, or esbuild no longer misinterpret your imports.
Your intent is explicit — other developers can immediately see which imports exist only for type checking.
It's not just a compiler flag: it's a form of code hygiene. It forces your imports to be truthful.
A peek under the hood
Before this flag existed, the compiler would rewrite imports during the "emit" phase. For example, if it thought an import was unused at runtime, it would silently drop it.
But in projects with multiple build tools, that caused mismatches: TypeScript would drop something that Babel or SWC still saw in the source, and your build pipeline would explode with missing modules. With verbatimModuleSyntax, the compiler becomes literal: it emits exactly what you wrote. No more heuristics, no more guessing.
You decide what the runtime is, and TypeScript respects it.
What happens when you turn it on
When we enabled this in our own codebase, it broke dozens of files, the TypeScript compiler went crazy to the point it was annoying. Then it was enlightening.
It turned out that we had hundreds of imports that were only used in type positions:
Once we marked them all with type, our emitted JavaScript shrank, our bundler logs got cleaner, and our mental model got simpler. We realized we had been lying to our tools all along. Not intentionally, but by omission.
The mental shift
The beauty of verbatimModuleSyntax is that it changes how you think about your code.
It reminds you that:
TypeScript's job is to validate, not to run.
Imports describe execution, not knowledge.
The boundary between those two should be sharp.
Every time you write import type, you make that boundary explicit. It's a small habit that prevents an entire class of build-time confusion.
The future: stricter TypeScript, cleaner builds
This flag isn't some niche setting: it's the direction TypeScript is heading. Frameworks like Next.js, Vite, and SvelteKit are already assuming you'll use it (or will enable equivalent behavior soon). The TypeScript team has been clear: explicit is better than inferred. That means:
If you maintain a library, enable it now.
If you work on an app, try it in a single module and see what breaks.
Everything that does break probably should have been fixed already.
A sharp edge: inline type can emit import {}
There's one subtle pitfall worth calling out. If you write an inline type-only named import and it's the only specifier, e.g.:
TypeScript may erase the binding at emit time and leave:
Depending on your environment and whether 'somewhere' actually exists and is resolvable at runtime, this can lead to confusing behavior or even a runtime error. More importantly, it creates a pointless side‑effect import when your intent was "types only."
There are safer patterns:
Prefer the dedicated type import form when the import is types‑only:
This is entirely erased in emitted JS.
If you truly need the module for its side effects and you need types from it, split the imports:
This avoids producing a stray import {} and keeps runtime intent explicit.
Automating the cleanup with ESLint
If you're thinking, "That's a lot of files to fix manually," you're right. Luckily, ESLint can do it for you. The rule @typescript-eslint/consistent-type-imports automatically enforces and can even rewrite imports to use import type where appropriate.
You can run it across your whole project with:
It's a simple way to clean up all those ghost imports in one go and keep your code consistent going forward.
Final thoughts
Turning on verbatimModuleSyntax feels like pulling back a curtain. You start seeing which parts of your code exist only in the type system, and which ones actually run. It's a small change that makes your mental model and your build process align for the first time.
TypeScript isn't your runtime. It just makes your runtime less painful. So make it tell the truth.




