ES module import/export in TypeScript
TypeScript's module syntax is exactly ECMAScript's import/export — nothing new to learn there. What TypeScript adds is checked at the boundary: the type checker verifies that what a file imports matches what the other file actually exports, and a separate form, import type, imports only a type with zero runtime trace.
export interface Point {
x: number
y: number
}
export function distance(a: Point, b: Point): number {
return Math.sqrt((a.x - b.x) ** 2 + (a.y - b.y) ** 2)
}
export const ORIGIN: Point = { x: 0, y: 0 }
// A default export — one per file, imported without braces
export default function midpoint(a: Point, b: Point): Point {
return { x: (a.x + b.x) / 2, y: (a.y + b.y) / 2 }
}import midpoint, { distance, ORIGIN, type Point } from "./math"
// ^ default ^ named ^ type-only named import
const here: Point = { x: 3, y: 4 }
console.log(distance(here, ORIGIN))
console.log(midpoint(here, ORIGIN))
// Import everything as one namespace object
import * as MathUtils from "./math"
MathUtils.distance(here, ORIGIN)
// Re-export: this file did not define Point, but other files can import it from here
export { type Point } from "./math"| Form | Syntax | When to reach for it |
|---|---|---|
| Named export/import | export function f() / import { f } from "./m" | The default. Most things a module exposes. |
| Default export/import | export default f / import f from "./m" | One obvious "main thing" per file — a component, a single class. |
| Namespace import | import * as M from "./m" | Importing many names from one module without listing each one. |
| Type-only import | import type { T } from "./m" | You only need T for annotations — guarantees the import is erased, never a runtime require. |
| Re-export | export { X } from "./m" | Building a public "barrel" file that gathers several modules' exports into one. |
import type, the compiler guarantees the import produces zero runtime code — the line disappears entirely from the emitted JavaScript. This matters when a bundler compiles files one at a time (isolatedModules, which esbuild, SWC and Next.js all require): the bundler cannot see across files to know an import was "only a type", so an ordinary import { Point } for a type-only value can crash the build. Write import type whenever the imported name is only ever used in a type position.You should see
5
5.0Which import form is guaranteed to produce zero runtime code once compiled?
import type can only ever bring in a type, so the compiler can always erase it. An ordinary import { X } might be a value or a type — TypeScript figures that out per-usage, but a per-file bundler often cannot, which is exactly why isolatedModules projects lean on import type explicitly.