14 — Modules and Declaration Merging: Organizing and Extending Types
"Just import and export" was my modules model, and it skipped the three other ideas in the chapter. Writing it down separated them: a module is a file-scoped unit that explicitly exports and imports its members, namespaces are the older logical-grouping mechanism, ambient modules describe the shape of existing JavaScript, and declaration merging is how TypeScript lets consumers add to existing types without editing the source. [1] Each solves a different organizational problem.
The starting point is that TypeScript (like modern JavaScript) organizes code into modules — files that encapsulate their own variables, functions, classes, and interfaces, preventing naming collisions [1]. Each module explicitly exports the parts it wants to share and imports what it needs:
// user.ts
export interface User { id: number; name: string; }
// app.ts
import { User } from "./user";Nothing leaks between modules unless exported. This is the same module system as modern JavaScript (ES modules), and it's the default way TypeScript expects code to be organized — one module per file, dependencies declared with import.
Namespaces: the older grouping mechanism
Namespaces are TypeScript's predecessor to modules — a way to group related code under a named container, declared with the namespace keyword [2]:
namespace Validation {
export function isEmail(value: string) { /* ... */ }
}
// used as Validation.isEmail(...)Namespaces predate ES modules and were the original answer to "how do I avoid global namespace pollution in a script-based codebase." In modern TypeScript, modules have almost entirely replaced namespaces — the official guidance is to use modules for new code [3]. Namespaces still appear in older libraries and in specific scenarios (global type definitions, declaration merging), so recognizing them matters, but reaching for them in new work is usually the wrong call.
The namespaces vs. modules distinction
The confusion worth clearing up: namespaces group code by a named container; modules group code by file. A namespace lives inside a file (or spans files via merging); a module is the file. With modules, the file system is the organization — import { X } from "./path" resolves to a file. With namespaces, the namespacing is manual — Validation.isEmail. Modules won because they leverage the file system and align with the JavaScript standard, leaving namespaces as a legacy tool. The handbook is explicit about this preference [3].
Ambient modules: describing JavaScript
Ambient modules let me describe the shape of existing JavaScript code that doesn't ship TypeScript types [4]. When I import a JavaScript library that has no .d.ts declaration file, the compiler complains it can't find types. An ambient module declaration tells the compiler "this module exists, and here's its shape":
declare module "legacy-lib" {
export function doThing(input: string): number;
}Now import { doThing } from "legacy-lib" type-checks. Ambient declarations are how TypeScript bridges to the untyped JavaScript ecosystem — they're the bridgehead, and in practice most popular libraries ship their own declaration files (.d.ts), so I only write ambient modules for internal or obscure JavaScript code. The DefinitelyTyped repository (@types/* packages) exists to provide these declarations for libraries that don't ship them.
External modules
The roadmap's "external modules" is simply the modern term for what I've been calling modules — files that export and import [5]. The name distinguishes them from the older namespace-based "internal modules," but in current TypeScript a module and an external module are the same thing: a file with top-level import or export statements. This terminology is mostly historical; what matters is the file-scoped export/import behavior.
Declaration merging and module augmentation
This is the feature that took longest to click. Declaration merging is TypeScript's ability to combine multiple declarations with the same name — and its most powerful application is module augmentation, adding to an existing module's types without modifying its source [6].
// my-augmentations.ts
import "express";
declare module "express" {
interface Request {
userId?: string; // adds a field to Express's Request
}
}After this, every Request object in my codebase has an optional userId — the augmentation merged into Express's own type. This is how middleware that attaches properties to request objects stays type-safe, and it's the mechanism behind framework plugins that extend core types. Global augmentation is the same idea but for globals — adding properties to window or built-in interfaces from anywhere in the project [7]. The pattern is powerful precisely because it lets third-party types grow to fit my codebase without forking the original definitions.
How I use this
My defaults: organize everything as modules (file-scoped exports/imports) and treat namespaces as legacy — I read them in old libraries but don't write them in new code. Use ambient modules only when importing untyped JavaScript that lacks a DefinitelyTyped package. Reach for module/global augmentation when I need to extend a library's types to reflect what my code (or middleware) adds — attaching fields to framework objects, extending third-party interfaces. The discipline that pays off is keeping augmentation declarations in dedicated, imported-for-side-effect files so they're discoverable, because augmented types are invisible if the augmentation file isn't included in the compile.
References
[1] Microsoft, "Modules," TypeScript Handbook, 2024. [Online]. Available: https://www.typescriptlang.org/docs/handbook/modules.html#handbook-content
[2] Microsoft, "Namespaces," TypeScript Handbook, 2024. [Online]. Available: https://www.typescriptlang.org/docs/handbook/namespaces.html
[3] Microsoft, "Namespaces and Modules," TypeScript Handbook, 2024. [Online]. Available: https://www.typescriptlang.org/docs/handbook/namespaces-and-modules.html
[4] Microsoft, "Ambient Modules," TypeScript Handbook, 2024. [Online]. Available: https://www.typescriptlang.org/docs/handbook/modules/reference.html#ambient-modules
[5] LearnCodeWeb, "TypeScript — External Module," 2023. [Online]. Available: https://learncodeweb.com/typescript/modules-in-typescript-explain-with-an-example/
[6] Microsoft, "Module Augmentation," TypeScript Handbook, 2024. [Online]. Available: https://www.typescriptlang.org/docs/handbook/declaration-merging.html#module-augmentation
[7] Microsoft, "Global augmentation," TypeScript Handbook, 2024. [Online]. Available: https://www.typescriptlang.org/docs/handbook/declaration-merging.html#global-augmentation
Knowledge check · Question 1 of 5
In modern TypeScript, the default way to organize code is…
Comments
Leave a Comment
You must be signed in to comment
0 Comments
No comments yet. Be the first to comment!