Alexandre Gaubert

Oct 22, 2025

7 min

read

TypeScript's import type: The Flag That Makes Builds Honest

Stay ahead in CI/CD

The latest blog posts, release news, and automation tips straight in your inbox

Stay ahead in CI/CD

The latest blog posts, release news, and automation tips straight in your inbox

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.

  1. TypeScript phase: The compiler parses your .ts files, checks types, and then erases them.

  2. Emit phase: It outputs .js files with only the executable code.

  3. Bundler phase: Vite or Webpack scans that JavaScript to build a dependency graph.

  4. 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:

// user.ts
import { User } from './types';

export function getUserName(u: User) {
  return u.name;
}

You'd expect this to compile down to something clean like:

export function getUserName(u) {
  return u.name;
}

But that's not what happens.

By default, TypeScript will keep the import statement in the emitted JavaScript:

import { User } from './types';
export function getUserName(u) {
  return u.name;
}

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:

{
  "compilerOptions": {
    "verbatimModuleSyntax": true
  }
}

Once you turn it on, this no longer compiles silently:

import { User } from './types';

You have to be explicit:

import type { User } from './types';

Now, the compiler recognizes that this import is type-only and removes it entirely from the emitted JavaScript.

// emitted JS
export function getUserName(u) {
  return u.name;
}

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:

import { Config } from './config';
import { ApiResponse } from './api';

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.:

import { type Something } from 'somewhere';

TypeScript may erase the binding at emit time and leave:

import {} from 'somewhere';

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:

    import type { Something } from 'somewhere';

    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:

    import 'somewhere';         // side effects (runtime)
    import type { Something } from 'somewhere'; // types only

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:

npx eslint . --fix

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.

Stay ahead in CI/CD

The latest blog posts, release news, and automation tips straight in your inbox

Stay ahead in CI/CD

The latest blog posts, release news, and automation tips straight in your inbox

Recommended blogposts

Oct 23, 2025

5 min

read

The Magic (and Mayhem) Behind Our Config Deprecation Transformers

We built a "self-healing" system that fixes deprecated configs by opening PRs automatically. It worked like magic, until it didn't. Here's what we learned about the thin line between elegant automation and uncontrollable complexity.

Guillaume Risbourg

Oct 23, 2025

5 min

read

The Magic (and Mayhem) Behind Our Config Deprecation Transformers

We built a "self-healing" system that fixes deprecated configs by opening PRs automatically. It worked like magic, until it didn't. Here's what we learned about the thin line between elegant automation and uncontrollable complexity.

Guillaume Risbourg

Oct 23, 2025

5 min

read

The Magic (and Mayhem) Behind Our Config Deprecation Transformers

We built a "self-healing" system that fixes deprecated configs by opening PRs automatically. It worked like magic, until it didn't. Here's what we learned about the thin line between elegant automation and uncontrollable complexity.

Guillaume Risbourg

Oct 23, 2025

5 min

read

The Magic (and Mayhem) Behind Our Config Deprecation Transformers

We built a "self-healing" system that fixes deprecated configs by opening PRs automatically. It worked like magic, until it didn't. Here's what we learned about the thin line between elegant automation and uncontrollable complexity.

Guillaume Risbourg

Oct 22, 2025

7 min

read

TypeScript's import type: The Flag That Makes Builds Honest

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.

Alexandre Gaubert

Oct 22, 2025

7 min

read

TypeScript's import type: The Flag That Makes Builds Honest

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.

Alexandre Gaubert

Oct 22, 2025

7 min

read

TypeScript's import type: The Flag That Makes Builds Honest

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.

Alexandre Gaubert

Oct 22, 2025

7 min

read

TypeScript's import type: The Flag That Makes Builds Honest

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.

Alexandre Gaubert

Oct 15, 2025

5 min

read

Should We Still Write Docs If AI Can Read the Code?

AI can explain what code does — but not why it does it. This post explores how documentation is evolving in the age of AI, and why writing down human intent is becoming one of the most practical forms of AI alignment.

Alexandre Gaubert

Oct 15, 2025

5 min

read

Should We Still Write Docs If AI Can Read the Code?

AI can explain what code does — but not why it does it. This post explores how documentation is evolving in the age of AI, and why writing down human intent is becoming one of the most practical forms of AI alignment.

Alexandre Gaubert

Oct 15, 2025

5 min

read

Should We Still Write Docs If AI Can Read the Code?

AI can explain what code does — but not why it does it. This post explores how documentation is evolving in the age of AI, and why writing down human intent is becoming one of the most practical forms of AI alignment.

Alexandre Gaubert

Oct 15, 2025

5 min

read

Should We Still Write Docs If AI Can Read the Code?

AI can explain what code does — but not why it does it. This post explores how documentation is evolving in the age of AI, and why writing down human intent is becoming one of the most practical forms of AI alignment.

Alexandre Gaubert

Curious where your CI is slowing you down?

Try CI Insights — observability for CI teams.

Curious where your CI is slowing you down?

Try CI Insights — observability for CI teams.

Curious where your CI is slowing you down?

Try CI Insights — observability for CI teams.

Curious where your CI is slowing you down?

Try CI Insights — observability for CI teams.