What does TypeScript's `verbatimModuleSyntax` flag change about how imports and exports are emitted, and which older options does it replace?
answer
- what you wrote is what you get
- no more deciding for you
- type modifier drops it, nothing else does
- side-effect imports stop disappearing
- replaced two deprecated options
basics
~10 sverbatimModuleSyntax 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.
solid answer
~50 sThe rule is deliberately simple: an import or export that carries the `type` modifier is dropped completely, and one that does not is left exactly as you wrote it. That removes TypeScript's *elision* heuristic, where the compiler decided for you whether an import was worth keeping. Two consequences follow. A module you import purely for its side effect is never silently dropped, because you wrote the statement and it survives. And importing a type without marking it is now an error — the compiler tells you the name resolves to a type and must be imported with a type-only import, since it can no longer quietly delete it for you. It also means the emitted module keeps the module *format* you wrote. Introduced in TypeScript 5.0, it replaced the two narrower options `importsNotUsedAsValues` and `preserveValueImports`, which were deprecated in favour of it.
code
typescript · 11 lines// registry.ts
export interface Plugin { name: string }
export const plugins: Plugin[] = [];
console.log("registry module evaluated");
// app.ts — with verbatimModuleSyntax enabled
import type { Plugin } from "./registry"; // erased entirely
import { plugins } from "./registry"; // emitted exactly as written
const p: Plugin = { name: "logger" };
plugins.push(p);go deeper
Know the one-line rule: imports written with the type keyword are dropped, everything else is emitted exactly as written. Recognise the error that asks you to use a type-only import and know the fix.
Explain what import elision was and why replacing a type-driven decision with a syntactic one lets any single-file tool reproduce the output. Name the two deprecated options it supersedes and state the TypeScript 5.0 boundary.
Show the failure it retires — a side-effecting module silently deleted because its only named binding was a type — and describe adopting it on a live codebase, where the errors cluster at modules that export both types and values.
Own the config policy. Decide whether the team writes explicit type imports as convention and lets the compiler enforce it, and weigh the churn against a predictable, tool-agnostic emit that survives a change of build pipeline.
## The heuristic it removes Before this flag, TypeScript's emit did something unusual for a compiler: it *deleted statements you wrote* based on how it judged the names were used. That is import elision. If every name in an import turned out to be used only in type positions, the whole statement disappeared from the output. Elision solves a real problem — an interface has no runtime existence, so importing it must not survive — but it makes emit depend on type analysis, and it produces two long-standing annoyances: - **Side effects vanish.** `import { Registry } from "./registry";` where `Registry` happens to be used only as a type will delete the statement, and with it the module's registration side effect. The code compiles, the tests pass locally, and something quietly stops being initialised. - **Per-file tools cannot reproduce it.** A tool that reads only one file cannot know whether `Registry` is a class or an interface, so it cannot make the same decision. ## The rule `verbatimModuleSyntax` replaces the heuristic with syntax: ```ts import type { User } from "./models"; // dropped entirely import { type User, createUser } from "./models"; // 'type User' removed, statement kept import { Registry } from "./registry"; // kept, exactly as written import "./polyfills"; // kept, exactly as written ``` Read it as: *what you wrote is what you get, minus the `type` bits*. The decision now depends on nothing but the text of the file in front of you, which is why any transpiler can make it identically. ## The errors it introduces Because the compiler will no longer delete a statement for you, it must instead stop you from writing one that cannot survive: ```ts import { User } from "./models"; // User is an interface ``` With the flag on, this is a compile error saying `User` is a type and must be imported using a type-only import. The equivalent diagnostic exists on the export side for a re-export that resolves to a type-only declaration. The fixes are the ones you would write anyway: `import type`, the inline `type` specifier, or `export type`. That is the trade this flag makes. You accept an explicitness requirement at every type-only import site, and in exchange the emitted module is predictable from a single file, and no statement of yours is ever deleted behind your back. ## What it replaced TypeScript previously offered two narrower knobs, both deprecated in TypeScript 5.0 in favour of this one: - `importsNotUsedAsValues` — controlled whether an import used only for types was an error, preserved, or removed. - `preserveValueImports` — kept value imports that elision would otherwise have removed. They interacted awkwardly and neither gave a rule that a per-file tool could follow. Do not reach for them in new configuration; `verbatimModuleSyntax` is the supported answer. ## How it relates to isolatedModules The two are complementary and frequently confused. `isolatedModules` is **checking-only**: it changes nothing about emit and simply refuses source constructs a per-file tool could not translate. `verbatimModuleSyntax` **changes emit**: it makes `tsc`'s own output follow the single-file rule. In practice, enabling `verbatimModuleSyntax` forces the explicit `type` markers that make most `isolatedModules` complaints unreachable, but the two still cover different ground — the module-shape and ambient-const-enum diagnostics belong to `isolatedModules`. ## Practical notes - Adopt it together with a convention of writing `import type` by hand; the flag then simply enforces what the team already does. - The errors on adoption are mechanical, and they cluster wherever types and values are imported from the same module. - Be careful about the file's module format. If TypeScript determines a file is CommonJS — a `.cts` file, or a file in a package with `"type": "commonjs"` under the Node module-resolution modes — the flag will not rewrite ESM syntax into `require` for you, and writing ESM syntax there is reported as an error; the CommonJS forms `import x = require("...")` and `export =` are what belong in such a file.
- With verbatimModuleSyntax on, what happens if you import an interface without the type keyword?It is a compile error. The compiler reports that the name resolves to a type and must be imported using a type-only import. Under the old behaviour it would have silently elided the statement; now that it never deletes what you wrote, it has to refuse the statement instead, because the emitted import would request an export that does not exist.
- Why is this flag good news for a module you import purely for its side effect?Because the statement is emitted exactly as written, so the import always runs. Under elision, an import whose named bindings turned out to be type-only was deleted along with the side effect, which is a bug that compiles cleanly and only shows up as something not being registered or patched at runtime.
- Should a new project still set importsNotUsedAsValues or preserveValueImports?No. Both were deprecated in TypeScript 5.0 in favour of verbatimModuleSyntax, which subsumes them under a single rule a per-file tool can also follow. Setting the old options in a modern config gets you deprecation diagnostics and a less predictable emit; use verbatimModuleSyntax alone.
saying these in an interview costs you the question
- Confuses it with isolatedModules, which changes no output
- Thinks it strips more code rather than deleting less
- Expects unmarked type imports to still be silently elided
- Claims it is only about bundle size
- Recommends importsNotUsedAsValues alongside it