Skip to content
TypeScript ts modules 4 min read

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 .js extension — 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

SyntaxImportsExample
import { a, b }Named exportsimport { area } from "./math.js"
import { a as x }Renamed named exportimport { area as a } from "./math.js"
import * as nsAll exports as a namespaceimport * as math from "./math.js"
import defThe default exportimport Calc from "./calc.js"
import "./styles.css"Side effect only, no bindingsimport "./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 .js extension 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 as to resolve naming conflicts instead of aliasing variables manually.
Last updated June 29, 2026
Was this helpful?