Export & Import
TypeScript uses the ECMAScript module (ESM) system for sharing code between files. Any file with a top-level import or export becomes a module, with its own scope; everything else is private to that file unless explicitly exported. This is the foundation of every TypeScript codebase: it keeps names from colliding, makes dependencies explicit, and lets the compiler and bundler tree-shake unused code. This page covers named exports, re-exports, the various import forms, and how a file becomes a module in the first place.
What makes a file a module
A .ts file with at least one top-level import or export is a module — its declarations are scoped to the file. A file with neither is treated as a script whose top-level declarations join the global scope, which is rarely what you want.
// math.ts — this is a module because it exports
export const PI = 3.14159;
export function area(radius: number): number {
return PI * radius ** 2;
}
If a file has no imports or exports but you still want it treated as a module (for example, a file that only runs side effects), add an empty export {} to opt in.
Named exports
The most common form is the named export. You can attach export to a declaration inline, or list names in an export block at the bottom of the file.
// shapes.ts
export interface Point {
x: number;
y: number;
}
export type Shape = "circle" | "square";
const TAU = Math.PI * 2;
function circumference(r: number): number {
return TAU * r;
}
// grouped export block
export { TAU, circumference };
Named exports are explicit and discoverable: editors autocomplete them at the import site, and renaming is safe because every reference is tracked.
Importing named exports
Import named exports with a matching braces list. You can pull in as many as you need, and rename any of them with as to avoid collisions.
import { PI, area } from "./math.js";
import { circumference as circ, Point } from "./shapes.js";
const a = area(2);
const c = circ(5);
const origin: Point = { x: 0, y: 0 };
To grab everything a module exports under a single object, use a namespace import:
import * as math from "./math.js";
console.log(math.PI, math.area(3));
In true ESM (
"module": "nodenext"or"esnext"), import specifiers for your own files must include the.jsextension — even though the source file is.ts. The compiler does not rewrite paths. With a bundler or"moduleResolution": "bundler", the extension is optional.
Re-exporting
A barrel file re-exports symbols from several modules so consumers can import from one place. Re-export with export ... from, optionally renaming or aggregating with *.
// index.ts — a barrel
export { area, PI } from "./math.js";
export { circumference, type Point } from "./shapes.js";
export * from "./vectors.js"; // re-export everything
export * as geometry from "./geo.js"; // namespaced re-export
Barrels improve ergonomics but can hurt tree-shaking and slow builds if overused, so keep them shallow and avoid deeply nested chains.
Import forms at a glance
| Syntax | Imports | Example |
|---|---|---|
import { a, b } | Named exports | import { area } from "./math.js" |
import { a as x } | Renamed named export | import { area as a } from "./math.js" |
import * as ns | All exports as a namespace | import * as math from "./math.js" |
import def | The default export | import Calc from "./calc.js" |
import "./styles.css" | Side effect only, no bindings | import "./polyfill.js" |
Side-effect and dynamic imports
Sometimes you import a module purely for its side effects (registering a polyfill, running setup) without binding any names:
import "./register-globals.js";
For code splitting or conditional loading, use the dynamic import() expression, which returns a Promise of the module namespace and is fully typed:
async function loadHeavy() {
const { renderChart } = await import("./charts.js");
renderChart();
}
Dynamic imports let bundlers split your app into chunks loaded on demand, keeping the initial bundle small.
Best Practices
- Prefer named exports — they are explicit, refactor-safe, and tree-shake cleanly.
- Add
export {}to turn a side-effect-only file into a module and avoid leaking globals. - In ESM projects (
nodenext), include the.jsextension in relative import paths. - Keep barrel files shallow; deep re-export chains slow builds and defeat tree-shaking.
- Use namespace imports (
import * as) sparingly — they pull in the whole module. - Reach for dynamic
import()to lazy-load large or rarely-used code. - Rename with
asto resolve naming conflicts instead of aliasing variables manually.