Modules
In modern JavaScript and TypeScript development, Modules are indispensable. They provide a mechanism to better organize code by splitting it into smaller, reusable pieces. With modules, you can define private and public members, thus preventing accidental access to internal details of the module.
Why Use Modules?
- Organization: Break down large codebases into smaller, manageable pieces.
- Reusability: Create reusable components that can be imported wherever needed.
- Scoping: Avoid global scope pollution by encapsulating code within modules.
- Dependency Management: Clearly specify dependencies and load them in the right order.
Exporting and Importing
Modules export members that should be accessible from other modules. Other modules can then import these members.
Named Exports
Export specific members from a module:
// math.ts
export function add(x: number, y: number): number {
return x + y;
}
export function subtract(x: number, y: number): number {
return x - y;
}
export const PI = 3.14159;
Import the exported members:
// app.ts
import { add, subtract, PI } from './math';
console.log(add(2, 3)); // 5
console.log(subtract(5, 2)); // 3
console.log(PI); // 3.14159
Renaming Exports and Imports
You can rename exports or imports using the as keyword:
// math.ts
function addition(x: number, y: number): number {
return x + y;
}
export { addition as add };
Default Exports
Each module can optionally export a default member, which is especially useful for modules that only export a single class or function:
// greeter.ts
export default class Greeter {
constructor(private name: string) {}
greet(): string {
return `Hello, ${this.name}!`;
}
}
Import the default export:
// app.ts
import Greeter from './greeter';
const greeter = new Greeter('World');
console.log(greeter.greet()); // Hello, World!
Combining Default and Named Exports
A module can have both a default export and named exports:
// calculator.ts
export default class Calculator {
add(x: number, y: number): number {
return x + y;
}
}
export const PI = 3.14159;
export function multiply(x: number, y: number): number {
return x * y;
}
// app.ts
import Calculator, { PI, multiply } from './calculator';
const calc = new Calculator();
console.log(calc.add(2, 3)); // 5
console.log(multiply(4, 5)); // 20
console.log(PI); // 3.14159
Importing Everything
You can import all exports from a module using the * as syntax:
// math.ts
export function add(x: number, y: number): number {
return x + y;
}
export function subtract(x: number, y: number): number {
return x - y;
}
export const PI = 3.14159;
// app.ts
import * as Math from './math';
console.log(Math.add(2, 3)); // 5
console.log(Math.subtract(5, 2)); // 3
console.log(Math.PI); // 3.14159
Re-exporting
You can re-export members from another module, which is useful for creating a central export point:
// operations/add.ts
export function add(x: number, y: number): number {
return x + y;
}
// operations/subtract.ts
export function subtract(x: number, y: number): number {
return x - y;
}
// operations/index.ts
export { add } from './add';
export { subtract } from './subtract';
// or use: export * from './add';
// app.ts
import { add, subtract } from './operations';
console.log(add(5, 3)); // 8
console.log(subtract(5, 3)); // 2
Dynamic Imports
TypeScript supports dynamic module loading using the import() syntax, which returns a Promise:
// Dynamic import based on condition
async function loadMath() {
if (someCondition) {
const math = await import('./math');
console.log(math.add(2, 3)); // 5
}
}
// Or with .then()
if (userNeedsAdvancedFeatures) {
import('./advancedFeatures').then(module => {
module.initialize();
});
}
This is useful for:
- Code splitting: Load modules only when needed
- Conditional loading: Load different modules based on runtime conditions
- Lazy loading: Defer loading until the module is actually needed
Type-Only Imports and Exports
TypeScript allows you to import or export only types, which are stripped out at runtime:
// types.ts
export interface User {
id: number;
name: string;
}
export type UserRole = 'admin' | 'user' | 'guest';
export class UserService {
getUser(id: number): User {
return { id, name: 'John' };
}
}
// app.ts
import type { User, UserRole } from './types'; // Type-only import
import { UserService } from './types'; // Regular import
const role: UserRole = 'admin';
const service = new UserService();
const user: User = service.getUser(1);
Type-only imports prevent accidentally importing values and make it clear that certain imports are only for type checking.
Namespaces vs. Modules
While TypeScript has another module-like system called “namespaces” (previously “internal modules”), the modern approach is to use ES6-style modules (often referred to as “external modules”).
Namespaces (legacy approach):
namespace MyNamespace {
export function doSomething() {
console.log("Doing something");
}
}
MyNamespace.doSomething();
Modules (modern approach):
// myModule.ts
export function doSomething() {
console.log("Doing something");
}
// app.ts
import { doSomething } from './myModule';
doSomething();
Use modules instead of namespaces for new projects. Namespaces are mainly kept for backward compatibility.