TypeScript Namespaces & Modules — Complete Guide
In this tutorial, you will learn about TypeScript Namespaces & Modules. We cover key concepts, practical examples, and best practices to help you master this topic.
TypeScript gives you two ways to organize code — namespaces (internal modules) for grouping related code within a file or across files, and ES modules for external dependency management with standard import/export syntax.
What You'll Learn
- Namespaces: syntax, nesting, multi-file
- ES modules: import, export, re-export
- Namespace vs module: when to use each
- Ambient declarations with
declare module - Module resolution and
import type
Why It Matters
Understanding the difference between namespaces and modules is crucial for organizing TypeScript code correctly. Using namespaces when modules are expected (or vice versa) leads to broken builds, unexpected globals, and confused colleagues.
Real-World Use
The Doda Browser extension API uses namespaces for its internal organization (browser.tabs, browser.storage, browser.runtime) — these are all ambient namespace declarations that describe the browser API without runtime code. External libraries like React use ES modules.
Learning Path
flowchart LR A[Utility Types] --> B[Namespaces & Modules] B --> C[Declaration Files] B --> D[You Are Here] C --> E[Type Manipulation] E --> F[Classes & OOP]
Namespaces
Namespaces group related code under a single global name:
namespace Utilities {
export function greet(name: string): string {
return `Hello, ${name}!`;
}
export function capitalize(str: string): string {
return str.charAt(0).toUpperCase() + str.slice(1);
}
export const VERSION = "1.0.0";
}
// Usage
console.log(Utilities.greet("Alice")); // Hello, Alice!
console.log(Utilities.VERSION); // 1.0.0
Think of a namespace like a folder — it keeps related files together and prevents name collisions with code outside the folder.
Nested Namespaces
namespace App {
export namespace Config {
export const API_URL = "https://api.example.com";
export const TIMEOUT = 5000;
}
export namespace Utils {
export function formatDate(date: Date): string {
return date.toISOString();
}
}
}
console.log(App.Config.API_URL); // https://api.example.com
console.log(App.Utils.formatDate(new Date())); // 2026-06-28T...
Multi-File Namespaces
File: src/shapes.ts
namespace Shapes {
export interface Shape {
area(): number;
}
}
File: src/circle.ts
/// <reference path="shapes.ts" />
namespace Shapes {
export class Circle implements Shape {
constructor(public radius: number) {}
area(): number { return Math.PI * this.radius ** 2; }
}
}
File: src/main.ts
/// <reference path="shapes.ts" />
/// <reference path="circle.ts" />
const circle = new Shapes.Circle(5);
console.log(circle.area()); // 78.54
The /// <reference> directive tells TypeScript to include the referenced file. This is the old-style approach — modern TypeScript prefers ES modules.
ES Modules
ES modules use import and export:
// math.ts
export function add(a: number, b: number): number {
return a + b;
}
export function subtract(a: number, b: number): number {
return a - b;
}
export const PI = 3.14159;
// main.ts
import { add, subtract, PI } from "./math.js";
console.log(add(5, 3)); // 8
console.log(PI); // 3.14159
Default Exports
// logger.ts
export default class Logger {
log(message: string): void {
console.log(`[LOG] ${message}`);
}
}
// main.ts
import Logger from "./logger.js";
const logger = new Logger();
logger.log("Hello");
Re-exporting
// index.ts — barrel file
export { add, subtract } from "./math.js";
export { default as Logger } from "./logger.js";
export * from "./types.js";
import type
Use import type when you only need the type, not the runtime value:
// types.ts
export interface User { name: string; age: number; }
export function createUser(name: string): User { return { name, age: 30 }; }
// main.ts
import type { User } from "./types.js"; // Only imports the type
import { createUser } from "./types.js"; // Only imports the runtime value
import type is erased during compilation — no runtime import is generated. This can reduce bundle size.
Namespace vs Module: When to Use Each
| Aspect | Namespace | Module |
|---|---|---|
| Scope | Global/ambient | File-based |
| Syntax | namespace X {} |
import/export |
| Bundle | Single global | Multiple files |
| Tree-shaking | Not supported | Supported |
| Legacy code | Common in old TS, DefinitelyTyped | Modern standard |
| Declaration files | Ambient namespace declarations | Module declarations |
Rule of thumb: Use ES modules for most code. Use namespaces only for:
- Declaring ambient types (like browser APIs)
- Organizing code in non-module projects
- Legacy code compatibility
Ambient Declarations
Ambient declarations describe types without providing implementation:
// globals.d.ts
declare namespace MyGlobalLib {
function init(config: Record<string, unknown>): void;
const VERSION: string;
}
// In any file, without importing:
MyGlobalLib.init({ debug: true });
console.log(MyGlobalLib.VERSION);
Ambient Module Declarations
// types/css.d.ts
declare module "*.css" {
const content: Record<string, string>;
export default content;
}
// types/env.d.ts
declare module "@env" {
export const API_URL: string;
export const NODE_ENV: "development" | "production";
}
Module Augmentation
You can add properties to existing modules:
// express.d.ts
import "express";
declare module "express" {
interface Request {
user?: {
id: string;
role: "admin" | "user";
};
}
}
Now in your route handlers:
import { Request, Response } from "express";
app.get("/profile", (req: Request, res: Response) => {
console.log(req.user?.id); // Typed!
});
Common Mistakes
1. Using Namespaces with ES Modules
Namespaces create global variables. ES modules create local scopes. Don't mix them:
// Bad: namespace inside a module file
export namespace Utils {
export function doStuff() {}
}
// Good: just export the function
export function doStuff() {}
2. Forgetting the .js Extension in Import Paths
TypeScript recommends using .js extensions in import paths, even for .ts files:
// Correct (even though it's a .ts file)
import { User } from "./types.js";
// Wrong (will cause issues with some bundlers)
import { User } from "./types";
3. Using import When You Mean import type
// Bad: imports the runtime value even though it's only used as a type
import { User } from "./types";
// Good: only imports the type
import type { User } from "./types";
4. Circular Dependencies
Module A imports from B, B imports from A. This leads to undefined values at runtime. Restructure to avoid cycles.
5. Not Using Barrel Files
Instead of importing from multiple files:
// Instead of:
import { User } from "./models/user";
import { Product } from "./models/product";
import { Order } from "./models/order";
// Create a barrel:
// models/index.ts
export { User } from "./user";
export { Product } from "./product";
export { Order } from "./order";
// Then:
import { User, Product, Order } from "./models/index.js";
6. Using export default on Named Exports
// Avoid default exports unless you have a single main export
// Bad: import Logger from "./logger"; // Can rename freely
// Good: import { Logger } from "./logger"; // Name must match
Practice Questions
What is the difference between a namespace and a module? A namespace creates a global object grouping related code. A module creates a file-level scope using import/export.
When should you use
import type? When you only need the type, not the runtime value. It's erased during compilation, reducing bundle size.What is an ambient declaration? A declaration that describes types without providing implementation, typically in
.d.tsfiles.What is module augmentation? Adding properties to an existing module's types without modifying the module itself.
Challenge: Create a barrel file for a utils directory that re-exports all utility functions. Then create an ambient declaration for a hypothetical window.__APP_CONFIG__ global variable.
FAQ
Mini Project: Modular Utility Library
// src/utils/math.ts
export function add(a: number, b: number): number { return a + b; }
export function multiply(a: number, b: number): number { return a * b; }
// src/utils/strings.ts
export function capitalize(s: string): string {
return s.charAt(0).toUpperCase() + s.slice(1);
}
export function truncate(s: string, max: number): string {
return s.length > max ? s.slice(0, max) + "..." : s;
}
// src/utils/index.ts — barrel
export { add, multiply } from "./math.js";
export { capitalize, truncate } from "./strings.js";
// src/index.ts
import { add, capitalize } from "./utils/index.js";
console.log(add(10, 20)); // 30
console.log(capitalize("hello")); // Hello
// Ambient declaration for global config
// src/types/global.d.ts
declare namespace AppConfig {
export const API_BASE: string;
export const VERSION: string;
export function isProduction(): boolean;
}
What's Next
Now learn about declaration files for consuming JavaScript libraries:
| Lesson | Description |
|---|---|
| {{< ref "/programming-languages/typescript/15-utility-types" >}} | Review utility types |
| {{< ref "/programming-languages/typescript/17-declaration-files" >}} | .d.ts files, declare, DefinitelyTyped |
| {{< ref "/programming-languages/typescript/18-type-manipulation" >}} | satisfies, branded types |
Built by the developers of Doda Browser, DodaZIP, and Durga Antivirus Pro.
Built by the developers of DodaTech
Doda Browser, DodaZIP & Durga Antivirus Pro