TypeScript Blitz Day 9: Module System & Namespaces
Master TypeScript's module system - ES modules, import/export patterns, barrel exports, type-only imports, module resolution, and modern code organization for scalable applications.
Goal
After this lesson you’ll organize code in a scalable way using ES modules and understand modern module patterns.
Theory Part (20 min)
1. ES Modules - import/export Basics
// user.ts - Named exports
export interface User {
id: number;
name: string;
email: string;
}
export function createUser(name: string, email: string): User {
return {
id: Math.random(),
name,
email
};
}
export const MAX_USERS = 1000;
// main.ts - Named imports
import { User, createUser, MAX_USERS } from './user';
const user: User = createUser("John", "john@example.com");
console.log(MAX_USERS);
2. Default vs Named Exports
// logger.ts - Default export (one per file)
export default class Logger {
log(message: string): void {
console.log(message);
}
}
// Or function
export default function log(message: string): void {
console.log(message);
}
// Import default - can use any name
import Logger from './logger';
import MyLogger from './logger'; // Same thing, different name
import log from './logger';
// Named exports (multiple per file)
// utils.ts
export function formatDate(date: Date): string {
return date.toISOString();
}
export function formatNumber(num: number): string {
return num.toFixed(2);
}
// Import named - must use exact names
import { formatDate, formatNumber } from './utils';
// Can rename with 'as'
import { formatDate as fd, formatNumber as fn } from './utils';
// Import all as namespace
import * as Utils from './utils';
Utils.formatDate(new Date());
// Mix default and named
// api.ts
export default class ApiClient {}
export const API_URL = "https://api.example.com";
// Import both
import ApiClient, { API_URL } from './api';
When to use:
- Default export: Main class/function in file, library entry point
- Named exports: Multiple related exports, better for tree-shaking
3. Re-exporting and Barrel Exports
// types.ts
export interface User {
id: number;
name: string;
}
export interface Post {
id: number;
title: string;
}
// services.ts
export class UserService {}
export class PostService {}
// index.ts - Barrel export (aggregates exports)
export * from './types';
export * from './services';
export { default as ApiClient } from './api';
// Now can import from one place
import { User, Post, UserService, PostService, ApiClient } from './index';
// Instead of multiple imports:
// import { User, Post } from './types';
// import { UserService, PostService } from './services';
// import ApiClient from './api';
// Selective re-export
export { User, Post } from './types'; // Only these, not all
// Rename on re-export
export { UserService as US } from './services';
4. Type-only Imports
// types.ts
export interface User {
id: number;
name: string;
}
export function createUser(): User {
return { id: 1, name: "John" };
}
// Import only the type (erased in JS)
import type { User } from './types';
// This won't be in compiled JS
const user: User = { id: 1, name: "Jane" };
// Import both type and value
import { createUser, type User } from './types';
// Why use 'import type'?
// 1. Makes it clear it's only for types
// 2. Helps bundlers with tree-shaking
// 3. Avoids circular dependency issues
// 4. Better compilation performance
// Export type-only
export type { User } from './types'; // Re-export only type
5. Module Resolution Strategies
// TypeScript has two main strategies: Classic and Node
// Classic (legacy, rarely used)
// import { x } from "./module"
// Looks for: ./module.ts, ./module.d.ts
// Node (default, mimics Node.js)
// import { x } from "./module"
// Looks for:
// 1. ./module.ts
// 2. ./module.tsx
// 3. ./module.d.ts
// 4. ./module/package.json (main field)
// 5. ./module/index.ts
// tsconfig.json
{
"compilerOptions": {
"moduleResolution": "node", // or "classic"
"baseUrl": "./src",
"paths": {
"@utils/*": ["utils/*"],
"@components/*": ["components/*"]
}
}
}
// Now can import with aliases
import { formatDate } from '@utils/format';
import { Button } from '@components/Button';
// Instead of relative paths
import { formatDate } from '../../utils/format';
6. Namespaces (Legacy)
// Namespaces - old way to organize code (pre-modules)
namespace Utils {
export function formatDate(date: Date): string {
return date.toISOString();
}
export function formatNumber(num: number): string {
return num.toFixed(2);
}
// Not exported - internal only
function helper(): void {}
}
// Usage
Utils.formatDate(new Date());
// Nested namespaces
namespace Company {
export namespace Employees {
export class Manager {}
export class Developer {}
}
export namespace Products {
export class Software {}
}
}
const dev = new Company.Employees.Developer();
// Why NOT to use namespaces:
// 1. ES modules are standard
// 2. Better tooling support for modules
// 3. Tree-shaking works better with modules
// 4. Can't code-split namespaces easily
// When you might see namespaces:
// - Legacy code
// - Ambient declarations
// - Global type definitions
7. Triple-slash Directives
// Triple-slash directives are compiler instructions
// Reference another file
/// <reference path="./types.d.ts" />
// Reference a library
/// <reference types="node" />
// Common use: declaration files
// globals.d.ts
/// <reference types="node" />
declare global {
namespace NodeJS {
interface ProcessEnv {
DATABASE_URL: string;
API_KEY: string;
}
}
}
export {};
// Modern alternative: Use tsconfig.json "types" field
{
"compilerOptions": {
"types": ["node", "jest"]
}
}
// When to use triple-slash:
// - Only in .d.ts files
// - Declaring dependencies between declaration files
// - Rare cases in modern TypeScript
8. Dynamic Imports
// Dynamic imports for code-splitting
async function loadModule() {
// Import returns a Promise
const module = await import('./heavy-module');
module.doSomething();
}
// Conditional loading
if (condition) {
const { SpecialFeature } = await import('./special-feature');
new SpecialFeature();
}
// Type-safe dynamic imports
type ModuleType = typeof import('./module');
async function getModule(): Promise<ModuleType> {
return import('./module');
}
// Lazy loading in React
const LazyComponent = React.lazy(() => import('./Component'));
Practice Part (25 min)
Exercise: Build a Modular User Management System
Create a well-organized module structure with proper imports/exports.
// TODO: Create the following file structure
// types/user.ts
export interface User {
id: number;
name: string;
email: string;
role: "admin" | "user";
}
export type UserId = number;
export type UserRole = User["role"];
// types/index.ts - Barrel export
// TODO: Re-export all from user.ts
// services/user-service.ts
// TODO: Create UserService class with:
// - getUser(id: UserId): Promise<User>
// - createUser(data: Omit<User, "id">): Promise<User>
// - updateUser(id: UserId, data: Partial<User>): Promise<User>
// services/auth-service.ts
// TODO: Create AuthService with:
// - login(email: string, password: string): Promise<User>
// - logout(): void
// - getCurrentUser(): User | null
// services/index.ts
// TODO: Barrel export both services
// utils/validators.ts
// TODO: Create validation functions:
// - isValidEmail(email: string): boolean
// - isValidRole(role: string): role is UserRole
// utils/formatters.ts
// TODO: Create formatter functions:
// - formatUserName(user: User): string
// - formatRole(role: UserRole): string
// utils/index.ts
// TODO: Barrel export with renamed exports
// index.ts - Main entry point
// TODO: Export everything from types, services, and utils
// Use proper re-exports
// main.ts - Usage example
// TODO: Import and use the modules
Solution:
Click to see solution
// types/user.ts
export interface User {
id: number;
name: string;
email: string;
role: "admin" | "user";
}
export type UserId = number;
export type UserRole = User["role"];
// types/index.ts
export * from './user';
// services/user-service.ts
import type { User, UserId } from '../types';
export class UserService {
private users: Map<UserId, User> = new Map();
private nextId: number = 1;
async getUser(id: UserId): Promise<User> {
const user = this.users.get(id);
if (!user) {
throw new Error("User not found");
}
return user;
}
async createUser(data: Omit<User, "id">): Promise<User> {
const user: User = {
id: this.nextId++,
...data
};
this.users.set(user.id, user);
return user;
}
async updateUser(id: UserId, data: Partial<User>): Promise<User> {
const user = await this.getUser(id);
const updated = { ...user, ...data };
this.users.set(id, updated);
return updated;
}
}
// services/auth-service.ts
import type { User } from '../types';
export class AuthService {
private currentUser: User | null = null;
async login(email: string, password: string): Promise<User> {
// Simplified - in real app would validate credentials
const user: User = {
id: 1,
name: "John Doe",
email,
role: "user"
};
this.currentUser = user;
return user;
}
logout(): void {
this.currentUser = null;
}
getCurrentUser(): User | null {
return this.currentUser;
}
}
// services/index.ts
export { UserService } from './user-service';
export { AuthService } from './auth-service';
// utils/validators.ts
import type { UserRole } from '../types';
export function isValidEmail(email: string): boolean {
return /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email);
}
export function isValidRole(role: string): role is UserRole {
return role === "admin" || role === "user";
}
// utils/formatters.ts
import type { User, UserRole } from '../types';
export function formatUserName(user: User): string {
return `${user.name} (${user.email})`;
}
export function formatRole(role: UserRole): string {
return role.charAt(0).toUpperCase() + role.slice(1);
}
// utils/index.ts
export {
isValidEmail as validateEmail,
isValidRole as validateRole
} from './validators';
export {
formatUserName as formatUser,
formatRole
} from './formatters';
// index.ts - Main entry point
export * from './types';
export * from './services';
export * from './utils';
// main.ts - Usage
import {
User,
UserService,
AuthService,
validateEmail,
formatUser,
formatRole
} from './index';
async function main() {
const userService = new UserService();
const authService = new AuthService();
// Create user
const newUser = await userService.createUser({
name: "Jane Doe",
email: "jane@example.com",
role: "admin"
});
console.log("Created:", formatUser(newUser));
console.log("Role:", formatRole(newUser.role));
// Login
if (validateEmail("jane@example.com")) {
const user = await authService.login("jane@example.com", "password");
console.log("Logged in:", user.name);
}
// Get current user
const current = authService.getCurrentUser();
console.log("Current user:", current?.name);
}
main();
Quick Review - Key Concepts
1. Default vs Named exports?
Default export:
- One per file
- Can rename on import
- Good for main class/function
// logger.ts
export default class Logger {}
// Import with any name
import Logger from './logger';
import MyLogger from './logger';
Named exports:
- Multiple per file
- Must use exact name (or rename with ‘as’)
- Better for tree-shaking
- More explicit
// utils.ts
export function format() {}
export function parse() {}
// Must use exact names
import { format, parse } from './utils';
import { format as f } from './utils'; // Rename
2. What is a barrel export?
Barrel export is an index.ts file that re-exports from multiple files for convenience.
// index.ts (barrel)
export * from './types';
export * from './services';
export { default as Api } from './api';
// Single import instead of multiple
import { User, UserService, Api } from './index';
// Instead of:
import { User } from './types';
import { UserService } from './services';
import Api from './api';
Benefits:
- Cleaner imports
- Hide internal structure
- Easy refactoring
Drawbacks:
- Can hurt tree-shaking if not careful
- May re-export unused code
3. What is type-only import?
Type-only import uses import type to import only types (erased in JS).
import type { User } from './types';
// Only for type annotation
const user: User = { id: 1, name: "John" };
// Won't be in compiled JavaScript
Benefits:
- Makes intent clear
- Better tree-shaking
- Avoids circular dependencies
- Faster compilation
Mixed imports:
// Import both
import { createUser, type User } from './types';
4. Namespaces vs Modules?
Modules (modern):
// file1.ts
export function doSomething() {}
// file2.ts
import { doSomething } from './file1';
Namespaces (legacy):
namespace Utils {
export function doSomething() {}
}
Utils.doSomething();
Use modules, not namespaces:
- ES standard
- Better tooling
- Tree-shaking
- Code-splitting
5. When to use triple-slash directives?
Triple-slash directives are compiler instructions:
/// <reference types="node" />
/// <reference path="./types.d.ts" />
Use only in:
- Declaration files (.d.ts)
- Declaring dependencies
- Legacy code
Modern alternative: Use tsconfig.json
{
"compilerOptions": {
"types": ["node", "jest"]
}
}
Checklist
- Use named exports for multiple exports
- Use default export for main class/function
- Create barrel exports for clean imports
- Use type-only imports when appropriate
- Configure module resolution in tsconfig
- Avoid namespaces (use modules instead)
- Understand when triple-slash directives are needed
- Use dynamic imports for code-splitting
Quick Reference
// Named exports
export interface User {}
export function createUser() {}
export const MAX = 100;
// Default export
export default class Api {}
// Import named
import { User, createUser } from './module';
// Import default
import Api from './module';
// Import all
import * as Module from './module';
// Rename
import { User as U } from './module';
// Type-only
import type { User } from './module';
// Re-export (barrel)
export * from './types';
export { default as Api } from './api';
// Dynamic import
const module = await import('./module');
// Module resolution (tsconfig.json)
{
"compilerOptions": {
"moduleResolution": "node",
"baseUrl": "./src",
"paths": {
"@utils/*": ["utils/*"]
}
}
}
// Path aliases
import { format } from '@utils/format';
Tomorrow: Async/Await & Promises - master asynchronous TypeScript