#Type-only imports and exports
An import in a TypeScript file can bring in two very different kinds of thing. Values — functions, classes, constants — exist at runtime, so the import has to survive into the JavaScript. Types — interfaces, type aliases — exist only while the compiler is checking. After type erasure there is nothing left to import, so the import has to disappear. The compiler has to decide, import by import, which is which.
By default tsc decides using import elision: it looks at how each imported name is used in the file, and if a name is only ever used in type positions, it drops it from the output. If every name in an import statement is dropped, the whole statement goes. That works when tsc compiles the whole project, because it can open ./user.ts and see that User is an interface. It does not work for tools that compile one file at a time — esbuild, swc, Babel, Vite and Node's built-in type stripping. Looking at import { User } from "./user.js" on its own, they can't tell whether User is a class (keep it) or an interface (delete it).
import type solves this by labelling the import. Think of the types as the paperwork that travels with a delivery: the compiler reads it, checks everything against it, and then shreds it before the truck (the JavaScript) leaves. import type stamps "paperwork" on the box, so any tool can shred it without opening it.
import type { IncomingMessage, ServerResponse } from "node:http";
import { createServer, type Server } from "node:http";
function handle(req: IncomingMessage, res: ServerResponse): void {
res.end(`You asked for ${req.url}`);
}
const server: Server = createServer(handle);- Line 1 is a type-only import declaration.
IncomingMessageandServerResponseare actually classes in Node, but this file only uses them to annotatereqandres, so their types are all it needs. The whole statement is removed from the output, whatever compiler settings you use. - Line 2 is a normal import with an inline
typemodifier on one name.createServeris a value — it's called at runtime — so it stays.type Serveris dropped. The emitted line isimport { createServer } from "node:http";. - Checking is unaffected:
req.urlis stillstring | undefined,res.endstill wants a string, andcreateServer(handle)still checks thathandlehas the right request-listener signature.typechanges what's emitted, never what's checked. - Because every type-only name is labelled, a single-file tool can strip them without knowing anything about
node:http. This file is safe to run through esbuild or Vite, or directly withnode server.ts.
The flip side: a name imported with type brings only its type meaning. A class name has two meanings — the instance type and the constructor value — and import type gives you the first one only.
import type { Server } from "node:http";
let s: Server; // fine: a type annotation
type ServerClass = typeof Server; // fine: a type query is still a type position
const created = new Server(); // ❌ TS1361: 'Server' cannot be used as a value because it was imported using 'import type'.let s: ServerusesServeras the instance type → allowed.typeof Serverin a type alias asks for the type of the constructor value. That sounds like a value, but the query is answered at compile time and erased, so TypeScript allows it underimport type.new Server()needs the real constructor at runtime, and the import that would provide it is guaranteed to be deleted. The compiler stops you: TS1361 "'Server' cannot be used as a value because it was imported using 'import type'".- Fix: import it as a value (
import { Server } from "node:http"). Nothing is lost — a value import of a class still gives you its type too.
isolatedModules and verbatimModuleSyntaxWhat actually gets emitted
Two settings decide how strict the compiler is about this:
isolatedModules— reports code that a single-file compiler can't handle correctly. The classic case is re-exporting a type withouttype:export { User } from "./user.js"fails with TS1205 "Re-exporting a type when 'isolatedModules' is enabled requires using 'export type'", because a single-file tool can't know whether to keep that re-export.verbatimModuleSyntax(TypeScript 5.0+) — replaces the guessing with one simple rule: imports withouttypeare kept exactly as written; anything markedtypeis deleted. Import elision is switched off. An import of a type without thetypekeyword is an error: TS1484 "'User' is a type and must be imported using a type-only import when 'verbatimModuleSyntax' is enabled."
The difference shows up when the imported module has a side effect:
export interface User { id: number; name: string }
export type Role = "admin" | "user";
console.log("user.ts loaded"); // a side effect at the top levelimport type { User } from "./user.js";
import { type Role } from "./user.js";
const u: User = { id: 1, name: "Ann" };
const r: Role = "admin";
console.log(u.name, r);Default (import elision) — app.js
const u = { id: 1, name: "Ann" };
const r = "admin";
console.log(u.name, r);
export {};verbatimModuleSyntax: true — app.js
import {} from "./user.js";
const u = { id: 1, name: "Ann" };
const r = "admin";
console.log(u.name, r);import type { User }is deleted in both modes — a type-only import declaration always disappears, including its module specifier. Nothing about./user.jssurvives from that line.import { type Role }is a normal import statement whose only name is markedtype. With import elision, the compiler removesRole, sees the braces are now empty, and deletes the statement too. It addsexport {}so the output is still an ES module.- With
verbatimModuleSyntax, only thetype Rolename is removed; the statement is kept as written, leavingimport {} from "./user.js". That still loadsuser.js. - Run them: the default output prints
Ann admin; the verbatim output printsuser.ts loadedfirst, thenAnn admin. Ifuser.tsregistered a plugin or a polyfill, the default build would silently skip it. - The rule to remember:
import type { X }removes the whole import;import { type X }removes only the name. If you want neither side effect nor import, useimport type. If you want to guarantee a module runs, write a plain side-effect import:import "./polyfills.js"is never removed in either mode.
Exports work the same way: export type { User } and export type { User } from "./user.js" are always removed, and TypeScript 5.0 added export type * from "./types.js" for re-exporting every type from a module.