Capítulo 24 de 36

Chapter 24: Namespaces and Modules — Choosing and Avoiding Pitfalls

Core Idea

Modules and namespaces are two distinct code-organization mechanisms with different trade-offs — modern TypeScript recommends ES Modules by default, and this chapter is mainly a pitfalls guide for people migrating from or mixing the two.

Key Concepts

  • Modules vs. namespaces, at a glance: modules depend on a loader/runtime (CommonJS, native ESM), give each file its own scope, and are the recommended default for modern code (Node.js treats them as the default too). Namespaces are TypeScript-specific named global objects — no loader dependency, can span multiple files and be concatenated via outFile, historically useful for <script>-tag-based web apps without a bundler, but they pollute the global namespace and make dependencies between files harder to see as an app grows.
  • /// <reference>-ing a module is a common mistake: the triple-slash reference directive is for pulling in ambient declaration files (.d.ts files with declare module "Name" {...}), not for referencing another module's implementation file — that's what import is for. The compiler resolves an import path by looking for .ts/.tsx/.d.ts files matching the path, and only falls back to an ambient module declaration if no real file is found; the reference tag exists to make sure that ambient declaration file is loaded into the program at all (this is how consumers of node.d.ts typically pull it in).
  • Needless namespacing inside modules: wrapping a module's exports in a namespace (export namespace Shapes { export class Triangle {} }) is redundant and confusing — a module is already its own scope, and the consumer decides what local name to bind the whole import to (import * as shapes from "./shapes"). Adding an inner namespace just forces an awkward extra .Shapes. hop (shapes.Shapes.Triangle) for no isolation benefit modules don't already provide on their own.
  • Modules never share scope, so pre-emptive namespacing to avoid collisions is unnecessary — two different modules can both export a Triangle without conflict, because each consumer imports and names them independently.
  • Module-to-output-file trade-off: TypeScript has a strict one-to-one mapping between a module source file and its emitted JS file — outFile (bundling multiple sources into one output) is only possible when targeting module: "amd" or module: "system", not commonjs/umd, since those formats don't support concatenating multiple modules into a single file the way AMD/SystemJS do.

Code Examples

// shapes.ts — don't do this:
export namespace Shapes {
  export class Triangle {}
}
// consumer: shapes.Shapes.Triangle — redundant extra hop

// shapes.ts — do this instead:
export class Triangle {}
// consumer: import * as shapes from "./shapes"; new shapes.Triangle()
  • What it demonstrates: dropping a redundant namespace wrapper around a module's own exports — the module boundary already provides the isolation a namespace would add.

Key Takeaways

  1. Default to ES Modules for new TypeScript code — namespaces are a legacy, script-tag-era organization tool, not a modern recommendation.
  2. Never namespace a module's own top-level exports — the module file itself is already the isolation boundary; the consumer picks the local binding name on import.
  3. Use import, not /// <reference>, to pull in another module's real implementation — reference tags are for ambient .d.ts declaration files only.
  4. outFile bundling is gated by module target — it doesn't work with commonjs/umd.

Connects To

  • Modules: the ES Module syntax and semantics referenced throughout.
  • Namespaces (next chapter): full treatment of the namespace feature this chapter mostly steers you away from for application code.
  • Type Declarations: ambient module declarations (declare module "x" {...}) referenced in the /// <reference> pitfall.