namespaces-and-modules
Namespaces and Modules in TypeScript:
Understanding Namespaces:
Namespaces are a TypeScript-specific way to organize code into named groups. They can encapsulate variables, functions, classes, and interfaces, preventing name conflicts.
namespace Payment {
export class Transaction {
// ...
}
export function process(amount: number) {
// ...
}
}
// Usage
const transaction = new Payment.Transaction();
Payment.process(100);
Modules for Code Organization:
Modules are the standard ECMAScript feature for grouping and encapsulating code. Each module has its own scope and communicates with other modules via exports and imports.
// In a file payment.ts
export class Transaction {
// ...
}
export function process(amount: number) {
// ...
}
// In another file, you can use the exported members:
import { Transaction, process } from './payment';
const transaction = new Transaction();
process(100);
Differences Between Namespaces and Modules:
Namespaces can be nested and don't require a module loader, while modules are top-level, file-based, and rely on module loaders like CommonJS or ES Modules.
// Namespaces example - in a single file:
namespace Payment {
export namespace Services {
export class PaymentGateway {
// ...
}
}
}
// Modules example - across multiple files:
// payment-gateway.ts
export class PaymentGateway {
// ...
}
// index.ts
import { PaymentGateway } from './payment-gateway';
Using Exports in Modules:
Modules export their members using the export
keyword. This allows other modules to import and reuse functionality.
// In a file called mathUtils.ts
export function add(x: number, y: number): number {
return x + y;
}
export function subtract(x: number, y: number): number {
return x - y;
}
Importing from Modules:
To use code from another module, you import the parts you need using the import
keyword. This can be a default export or named exports.
// Assuming the above exports are in mathUtils.ts
import { add, subtract } from './mathUtils';
console.log(add(5, 3)); // 8
console.log(subtract(5, 3)); // 2
Refactoring from Namespaces to Modules:
As projects grow, it's common to refactor from namespaces to modules. This involves moving from a single or nested namespaces to multiple files, each representing a module.
// Original namespace code:
namespace Utilities {
export function log(message: string) {
console.log(message);
}
// Other utilities...
}
// Refactored module code:
// log.ts
export function log(message: string) {
console.log(message);
}
// Usage after refactoring:
import { log } from './utilities/log';
log('Message');
Ambient Namespaces for Legacy Code:
For working with existing JavaScript libraries, you can declare ambient namespaces which don't require an implementation and are used for type checking.
// Declaration for a legacy library
declare namespace LegacyLibrary {
function doSomething(): void;
}
// Usage in TypeScript without importing anything
LegacyLibrary.doSomething();
These examples and explanations outline the key concepts of namespaces and modules, emphasizing their differences and use cases in TypeScript for organizing code and ensuring type safety.
Single Responsibility Principle:
Both namespaces and modules should ideally adhere to the single responsibility principle, meaning they should only represent one functionality or domain.
// In a payment module, you would have all related payment functions, classes, etc.
export class PaymentProcessor {
// ...
}
export function refundTransaction() {
// ...
}
Avoiding Global Namespace Pollution:
Modules help avoid global namespace pollution by containing all their members within their scope unless explicitly exported.
// In a module, everything is local to the module unless exported
function localFunction() {} // This is not available outside the module
export function publicFunction() {} // This is available outside the module
Module Resolution Strategies:
TypeScript offers different module resolution strategies like Node
and Classic
, and understanding these is important for proper module importing.
// In tsconfig.json, you can specify the module resolution strategy
{
"compilerOptions": {
"moduleResolution": "node"
}
}
Namespaces for Logical Grouping:
While modules are file-based, namespaces can be used within files to logically group classes or interfaces that are closely related.
namespace Validation {
export class EmailValidator {
// ...
}
export class ZipCodeValidator {
// ...
}
}
Barrel Exports:
To simplify imports, you can create a "barrel" module that re-exports selected exports from other modules, allowing consumers to import from a single location.
// In index.ts, which acts as a barrel
export * from './emailValidator';
export * from './zipCodeValidator';
Isolating Modules with Namespaces:
If you have multiple modules that should not be globally available, you can encapsulate them within a namespace to avoid exposing them at the top level.
namespace InternalModules {
export module PrivateModule {
// Module contents
}
}
Using Declaration Merging with Namespaces:
TypeScript's declaration merging allows you to split the same namespace across multiple files. This can be useful for organizing code and maintaining backwards compatibility.
// In file1.ts
namespace Shared {
export class Util {
// ...
}
}
// In file2.ts
namespace Shared {
export class Helper {
// ...
}
}
Dynamic Module Loading:
TypeScript supports dynamic import()
expressions, which allow you to load modules on demand. This can improve performance by reducing the initial load time of your application.
async function loadModule() {
const module = await import('./myModule');
module.publicFunction();
}
Understanding and applying these concepts can significantly enhance the structure and maintainability of TypeScript applications, aligning with best practices in software development.