verbatimModuleSyntax

Emits every import and export exactly as written unless it carries a type modifier.

Signature
"verbatimModuleSyntax": boolean
  • Added in TypeScript 5.0

Parameters

ParameterTypeDescription
$trueoptionalbooleanAny import or export without a `type` modifier is emitted verbatim; anything marked `type` is dropped entirely. ES import/export are never rewritten to require() based on the module setting — use import = require() and export = for CommonJS. Implies isolatedModules.
$falseoptionalbooleanDefault. TypeScript performs import elision, removing imports it determines are only used for types.

Return value

Disables import elision: imports and exports without a type modifier survive into the output, and those with one are erased.

Errors

Errors when an ES import or export appears in a file that would be emitted as CommonJS (error TS1286/TS1287), since the syntax cannot be preserved verbatim there. Also errors on a value import used only as a type when the emit format demands the distinction.

Changelog

  • PHP 5.0Introduced, deprecating importsNotUsedAsValues and preserveValueImports, whose combined edge cases it replaces with one rule.
  • PHP 5.0Implies isolatedModules, because verbatim emit is exactly what a single-file transpiler can reproduce.

By default TypeScript performs import elision: it inspects every import, works out whether the binding is used as a value anywhere, and quietly deletes the import if it is not. That requires whole-program type information, and it produces surprises — a module whose side effects you relied on vanishes because you only used its types.

The one rule

verbatimModuleSyntax replaces all of that with: anything without a type modifier stays, anything with one goes. What you see is what you get.

// Erased away entirely.
import type { A } from "a";

// Rewritten to 'import { b } from "bcd";'
import { b, type c, type d } from "bcd";

// Rewritten to 'import {} from "xyz";'
import { type xyz } from "xyz";

That last line matters: the bare import {} from "xyz" survives, so the module is still loaded and its side effects still run.

The CommonJS restriction

Because the syntax is preserved literally, ES import/export are no longer rewritten into require calls for a CommonJS target — you get an error instead. To emit CommonJS you must write TypeScript's pre-ES2015 syntax, import foo = require("foo") and export = { … }. That is deliberate: it stops you accidentally shipping CommonJS because a package.json lacked "type": "module".

Example

Example · typescript
// tsconfig.json
//   { "compilerOptions": { "verbatimModuleSyntax": true, "module": "esnext" } }

import { type Cents, formatCents } from "./money.js";
import type { Currency } from "./currency.js";

// Emitted verbatim, minus the type-marked parts:
//   import { formatCents } from "./money.js";
//   (the `import type` line disappears completely)

const amount: Cents = 1999;
const code: Currency = "USD";

console.log(`${code} ${formatCents(amount)}`);
// -> USD $19.99

When to use it

  • Guaranteeing that esbuild, swc or Babel produce byte-identical output to tsc.
  • Keeping a side-effect-only import from being elided because it looked type-only.
  • Migrating off the deprecated importsNotUsedAsValues and preserveValueImports flags.
  • Making the ESM/CommonJS boundary explicit so a missing "type": "module" fails loudly.

Frequently asked questions

What does verbatimModuleSyntax do?

It turns off import elision: imports and exports without a type modifier are emitted exactly as written, and those marked type are removed entirely. This makes tsc's output match what single-file transpilers such as esbuild and Babel produce.

Should I use verbatimModuleSyntax or importsNotUsedAsValues?

Use verbatimModuleSyntax — importsNotUsedAsValues and preserveValueImports were deprecated in TypeScript 5.0 in its favour. It replaces their overlapping, hard-to-reason-about behaviour with a single rule.

Why do I get an error about ES imports in a CommonJS file?

Because verbatimModuleSyntax will not rewrite import/export into require calls, so ES syntax is rejected in a file that emits CommonJS. Either mark the package as "type": "module", or write import foo = require("foo") and export = instead.

More examples

Side effects survive an all-type import

With elision, an import whose bindings are all types is deleted, taking the module's side effects with it — a classic cause of "works in dev, breaks in the bundle". Verbatim emit leaves an empty import behind, so the module is still evaluated.

Example · typescript
// ./register.ts registers a global polyfill AND exports a type.
import { type Handler } from "./register.js";

const handler: Handler = (msg) => console.log(`handled: ${msg}`);
handler("ping");

// verbatimModuleSyntax: true  ->  import {} from "./register.js";
//                                 module still runs. ✔
// verbatimModuleSyntax: false ->  import elided entirely;
//                                 the polyfill never registers. ✘

// -> handled: ping

Emitting CommonJS under the flag

verbatimModuleSyntax refuses to silently convert import/export into require/module.exports, so CommonJS must be written as CommonJS. The payoff is that a file's emitted module format is always visible from its own source.

Example · typescript
// A .cts file, or a package without "type": "module".
// ES syntax here would be an error, so use the pre-ES2015 forms:

import foo = require("foo");

function parse(input: string) { return input.trim(); }
function format(input: string) { return input.toUpperCase(); }

export = { parse, format };

// Emitted:
//   const foo = require("foo");
//   function parse(input) { return input.trim(); }
//   function format(input) { return input.toUpperCase(); }
//   module.exports = { parse, format };

console.log(format(parse("  ok  ")));
// -> OK

Discussion

  • Be the first to comment on this lesson.