Type-Only Imports & Exports
Types exist only at compile time — they are erased before your code runs. When you import something used purely as a type, TypeScript can drop that import entirely from the emitted JavaScript. The import type and export type syntax makes this intent explicit, prevents accidental runtime dependencies, and avoids subtle bugs with side-effecting modules and transpilers like esbuild and SWC. This page covers type-only imports and exports, inline type modifiers, and the verbatimModuleSyntax option that ties it all together.
The problem type-only imports solve
A plain import of something used only as a type may or may not survive compilation, depending on settings. Tools that transpile files in isolation (esbuild, Babel, SWC) cannot see other files’ types, so they cannot know an import is type-only — and may keep or drop it incorrectly.
import { User } from "./models.js"; // is this a value or a type?
function greet(u: User): string { // used only as a type
return `Hi ${u.name}`;
}
Marking the import as type-only removes all ambiguity and guarantees it is erased.
import type and export type
Prefix the statement with type to declare it carries types only. The compiler guarantees the import emits no runtime code, and it errors if you try to use the binding as a value.
import type { User } from "./models.js";
export type { Config } from "./config.js";
const u: User = { name: "Ada" };
// ❌ Error: 'User' cannot be used as a value because it was imported using 'import type'.
new User();
export type { ... } does the same for re-exports, keeping a barrel file’s type re-exports out of the runtime graph.
Inline type modifiers
When a single statement mixes values and types, you do not need two separate imports. Apply type to individual specifiers inline (TypeScript 4.5+).
import { createUser, type User, type Role } from "./users.js";
const role: Role = "admin";
const u: User = createUser("Grace", role);
The same inline modifier works on exports:
export { renderTable, type TableProps } from "./table.js";
This keeps related imports together while still erasing the type-only parts.
verbatimModuleSyntax
The verbatimModuleSyntax compiler option (TS 5.0+, replacing the older importsNotUsedAsValues and isolatedModules emit behavior) enforces a simple rule: any import or export not marked type is emitted verbatim, and any import type is fully erased. This makes the output predictable and isolated-transpiler-safe.
// tsconfig.json
{
"compilerOptions": {
"verbatimModuleSyntax": true
}
}
With it on, you must use import type for type-only imports, or the import is kept at runtime:
// with verbatimModuleSyntax: true
import { User } from "./models.js"; // KEPT at runtime (may error if no runtime export)
import type { User } from "./models.js"; // correctly erased
verbatimModuleSyntaxalso forbids mixing CommonJSexport =with ESM syntax in the same file. It is the recommended setting for new ESM projects and is enabled by many starter templates.
Behavior comparison
| Syntax | Emitted to JS? | Can use as value? | Use when |
|---|---|---|---|
import { X } | Yes (if used) | Yes | X is a value (function, class, const) |
import type { X } | Never | No | X is used only in type positions |
import { type X, y } | Only y | X no, y yes | One statement mixes both |
export type { X } | No | n/a | Re-exporting a type from a barrel |
Why it matters for bundlers
Isolated transpilers compile one file at a time without the whole type graph. If a type-only import is not marked, the transpiler may emit import "./models.js", pulling in a module for its side effects and bloating the bundle — or it may error because no runtime export exists. Explicit import type removes the guesswork and keeps builds correct and lean.
// safe across tsc, esbuild, swc, babel
import type { Theme } from "./theme.js";
import { applyTheme } from "./theme.js";
Best Practices
- Enable
verbatimModuleSyntax: truein new projects for predictable, transpiler-safe emit. - Use
import typewhenever a binding is used only in type positions. - Prefer inline
typemodifiers to keep related value and type imports in one statement. - Use
export typefor type re-exports in barrel files to keep them out of the runtime graph. - Never call or instantiate a binding imported with
import type— it does not exist at runtime. - Treat type-only syntax as essential when building with esbuild, SWC, or Babel.