← thecodex.expert · The Codex Family of Knowledge
TypeScript

Modules and Namespaces

The official guidance is blunt: prefer modules to namespaces — namespace predates ES modules and mostly exists today for narrower legacy cases.

TypeScript 6.0 Prefer modules over namespaces Last verified:
Where this bites you

A file with no top-level import or export isn't a module, it's a script, and every script in the project shares one global scope. Two unrelated files that each declare const config then collide, and the error appears in both files at once, even though neither imports the other. It tends to show up the moment a second small file is added to a project: a helper with no imports yet, or a quick test file.

TypeScriptusers.ts + orders.ts
// users.ts
const config = { table: "users" };
console.log("users table:", config.table);

// orders.ts
const config = { table: "orders" };
console.log("orders table:", config.table);

// tsc --module esnext --moduleResolution bundler (or --module commonjs):
// orders.ts(1,7): error TS2451: Cannot redeclare block-scoped variable 'config'.
// users.ts(1,7): error TS2451: Cannot redeclare block-scoped variable 'config'.
Canonical Definition

Standard ES modules — import/export — are TypeScript's normal, recommended way to organize code across files, working identically to how the rest of the JavaScript ecosystem does it. namespace is an older, TypeScript-only construct predating ES modules in the JavaScript ecosystem, using dot-separated global names to group code; the official TypeScript documentation itself now recommends modules for ordinary application code, reserving namespaces for narrower cases like organizing global ambient type declarations.

ES modules: the standard, recommended approach

Any file with a top-level import or export is treated as a module, with its own scope — nothing it declares leaks into the global scope, unlike a plain script file.

TypeScriptmath.ts
export function add(a: number, b: number): number {
    return a + b;
}

export interface Point {
    x: number;
    y: number;
}
TypeScriptapp.ts
import { add, Point } from "./math";

const sum = add(2, 3);
const p: Point = { x: 1, y: 2 };

That's the rule above (a top-level import or export makes a file a module) seen from the other side. The smallest fix is to give each file an empty export, which turns it into a module with its own scope. Alternatively, "moduleDetection": "force" in tsconfig.json treats every non-declaration file as a module. Whether you hit this at all depends on your settings: with tsc 6.0.3, the files above collided under module: esnext/bundler, commonjs and preserve, but not under module: nodenext, where module detection was effectively forced in the same test.

TypeScriptusers.ts (same change in orders.ts)
const config = { table: "users" };
console.log("users table:", config.table);

export {};   // now a module: config is local to this file

namespace: an older, TypeScript-only alternative

namespace groups related code under a single global-ish name using dot notation — it predates ES modules being widely usable in the JavaScript ecosystem, and today mostly appears in older codebases or specific library-declaration scenarios.

TypeScriptnamespace_example.ts
namespace Validation {
    export function isValid(input: string): boolean {
        return input.length > 0;
    }
}

Validation.isValid("hello");   // accessed via dot notation, not import

Why the official guidance favors modules

Modules are standard JavaScript (no TypeScript-specific runtime behavior involved), integrate cleanly with bundlers and tree-shaking, and are what the entire modern JavaScript/TypeScript ecosystem is built around — this is precisely why the TypeScript documentation itself says to prefer modules to namespaces for ordinary application code, reserving namespace for the narrow cases (like organizing global ambient declaration files) where it genuinely still fits.

Sources

1
Microsoft. TypeScript Handbook, "Modules" and "Namespaces", typescriptlang.org/docs/handbook/2/modules.html.