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.
// 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'.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.
export function add(a: number, b: number): number {
return a + b;
}
export interface Point {
x: number;
y: number;
}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.
const config = { table: "users" };
console.log("users table:", config.table);
export {}; // now a module: config is local to this filenamespace: 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.
namespace Validation {
export function isValid(input: string): boolean {
return input.length > 0;
}
}
Validation.isValid("hello"); // accessed via dot notation, not importWhy 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.