Modules (import/export) & Namespaces

beginner modules namespaces import export

In simple language, a module is a file that exports stuff so other files can import it. TypeScript uses the same ES module syntax JavaScript uses — import / export. Namespaces are an older TS-only way to organize code that we should know about but rarely use in new code.

Modules — the standard way

Any file with a top-level import or export is a module. Without one, the file is a “script” and everything declared in it goes into the global scope (which is usually bad).

// math.ts — named exports
export function add(a: number, b: number) {
  return a + b;
}
export const PI = 3.14159;

// default export — one per file
export default class Calculator {
  add(a: number, b: number) { return a + b; }
}
// app.ts — importing
import Calculator, { add, PI } from "./math";
import * as math from "./math";        // namespace import
import { add as plus } from "./math";  // rename

console.log(add(1, 2));
console.log(math.PI);

The only difference between named and default exports: default exports are unnamed (the importer picks the name), named exports require matching the name (unless renamed with as).

Type-only imports

TS adds one thing on top of ES modules — type-only imports. These get erased at compile time and don’t generate any runtime require/import.

import type { User } from "./types";
import { type Config, loadConfig } from "./config"; // mixed

// type-only — totally removed at runtime
function process(user: User) { /* ... */ }

Two reasons to use import type: it makes intent clear, and it avoids accidental circular runtime dependencies. With verbatimModuleSyntax: true in tsconfig, TS enforces type-only imports for type-only usage.

Re-exports

We can forward exports from one module through another. Useful for building a “barrel” file (single entry point for a folder).

// utils/index.ts
export { add, PI } from "./math";
export * from "./strings";              // re-export everything
export { default as Calc } from "./math"; // re-export default as named

Now import { add } from "./utils" works. Be careful — barrel files can hurt tree-shaking if not set up right.

Namespaces — the legacy approach

Before ES modules existed, TS had namespace (originally called “internal modules”). They group related code under a name. We mostly see them in old .d.ts files and library typings.

namespace Geometry {
  export interface Point { x: number; y: number }
  export function distance(a: Point, b: Point) {
    return Math.hypot(a.x - b.x, a.y - b.y);
  }
}

const p: Geometry.Point = { x: 0, y: 0 };
Geometry.distance(p, { x: 3, y: 4 }); // 5

Namespaces can span multiple files (they merge by name), which sounds neat but creates implicit dependencies and breaks tree-shaking. Don’t use them in new code. Use ES modules.

ES Modules (use these)
• explicit imports / exports
• one file = one module
• tree-shakeable
• tooling support everywhere
Namespaces (legacy)
• merge across files
• one global per namespace
• poor tree-shaking
• fine in .d.ts only

CommonJS interop

Node’s classic require / module.exports is CommonJS. When importing a CJS module into a TS ES module project, we hit a snag: default-export interop.

// works when esModuleInterop is true (default in modern setups)
import express from "express";

// without esModuleInterop, we'd need:
import * as express from "express";

esModuleInterop: true in tsconfig makes CJS modules look like they have default exports. Modern setups have this on — it’s only relevant if we’re maintaining an old codebase.

Module resolution gotcha

When TS sees import "./math", how does it find the file? That’s controlled by moduleResolution in tsconfig. For modern Node + bundlers, use "bundler" or "node16". With "node16" we need explicit .js extensions in imports (even though the file is .ts) — confusing but correct, because that’s what ES modules require at runtime.

// with moduleResolution: "node16" + ESM
import { add } from "./math.js"; // .js even though file is math.ts

Stick with "bundler" if a bundler (Vite, esbuild, webpack) handles resolution. Use "node16" for native Node ESM.