Modules are the foundation of code organization in TypeScript. They let you split your code into separate files, export functionality from one file, and import it into another. TypeScript uses ES6 module syntax (import/export) which keeps your codebase maintainable and scalable. Let's master TypeScript modules!
Named Exports and Imports
Named exports allow you to export multiple items from a file. Each export must be imported using its exact name (or aliased).
// Named exports - multiple exports from one file
export interface User {
id: number;
name: string;
email: string;
}
export class UserService {
private users: User[] = [];
addUser(user: User): void {
this.users.push(user);
}
getUser(id: number): User | undefined {
return this.users.find(u => u.id === id);
}
getAllUsers(): User[] {
return [...this.users];
}
}
export function createUser(name: string, email: string): User {
return {
id: Date.now(),
name,
email
};
}
export const ADMIN_ROLE = "admin";
export const USER_ROLE = "user";
// Private (not exported) - only available in this file
function validateEmail(email: string): boolean {
return email.includes("@");
}// Importing named exports
import { User, UserService, createUser, ADMIN_ROLE } from './user';
const service = new UserService();
const user1 = createUser("Yusuf Ibrahim", "yusuf@example.com");
const user2 = createUser("Amina Hassan", "amina@example.com");
service.addUser(user1);
service.addUser(user2);
console.log(service.getAllUsers());
// Import with alias
import { ADMIN_ROLE as AdminRole, USER_ROLE as RegularUser } from './user';
console.log(AdminRole); // "admin"
console.log(RegularUser); // "user"
// Import all named exports as namespace
import * as UserModule from './user';
const user3 = UserModule.createUser("Chidi Okafor", "chidi@example.com");
const service2 = new UserModule.UserService();
service2.addUser(user3);
console.log(UserModule.ADMIN_ROLE); // "admin"Default Exports and Imports
Default exports allow you to export one main item from a file. It can be imported with any name (no curly braces needed).
// Default export - one main export per file
export default class Logger {
private logs: string[] = [];
log(message: string): void {
const timestamp = new Date().toISOString();
const logEntry = `[${timestamp}] ${message}`;
this.logs.push(logEntry);
console.log(logEntry);
}
error(message: string): void {
this.log(`ERROR: ${message}`);
}
getLogs(): string[] {
return [...this.logs];
}
clear(): void {
this.logs = [];
}
}// Importing default export (can use any name)
import Logger from './logger';
const logger = new Logger();
logger.log("Application started");
logger.error("Something went wrong");
console.log(logger.getLogs());
// Can name it anything
import MyLogger from './logger';
const customLogger = new MyLogger();
customLogger.log("Custom logger");
// Default export with function
// File: math.ts
export default function add(a: number, b: number): number {
return a + b;
}
// File: app.ts
import add from './math';
// OR
import sum from './math'; // Different name, same function
console.log(add(5, 3)); // 8
console.log(sum(10, 20)); // 30
// Default export with arrow function
// File: multiply.ts
const multiply = (a: number, b: number): number => a * b;
export default multiply;
// File: app.ts
import multiply from './multiply';
console.log(multiply(4, 5)); // 20Mixing Default and Named Exports
You can have both a default export and named exports in the same file.
// Default export + named exports
export interface DatabaseConfig {
host: string;
port: number;
database: string;
}
export interface AppConfig {
name: string;
version: string;
database: DatabaseConfig;
}
// Default export
export default class Config {
private static instance: Config;
private constructor(
public app: AppConfig
) {}
static getInstance(app: AppConfig): Config {
if (!Config.instance) {
Config.instance = new Config(app);
}
return Config.instance;
}
getDatabaseConfig(): DatabaseConfig {
return this.app.database;
}
}
// Named exports
export const DEFAULT_PORT = 5432;
export const DEFAULT_HOST = "localhost";
export function createDefaultConfig(appName: string): AppConfig {
return {
name: appName,
version: "1.0.0",
database: {
host: DEFAULT_HOST,
port: DEFAULT_PORT,
database: appName.toLowerCase()
}
};
}// Importing default and named exports
import Config, {
AppConfig,
DatabaseConfig,
DEFAULT_PORT,
createDefaultConfig
} from './config';
// Use named exports
const appConfig = createDefaultConfig("MyApp");
console.log(`Default port: ${DEFAULT_PORT}`);
// Use default export
const config = Config.getInstance(appConfig);
const dbConfig = config.getDatabaseConfig();
console.log(`Database: ${dbConfig.host}:${dbConfig.port}/${dbConfig.database}`);
// "Database: localhost:5432/myapp"
// Alternative: Import default separately
import ConfigClass from './config';
import { createDefaultConfig as createConfig } from './config';
const config2 = ConfigClass.getInstance(createConfig("AnotherApp"));Re-exporting Modules
Re-exporting allows you to create a single entry point for multiple modules, making imports cleaner.
export interface User {
id: number;
name: string;
email: string;
}
export class UserModel {
constructor(public user: User) {}
getFullInfo(): string {
return `${this.user.name} (${this.user.email})`;
}
}export interface Product {
id: number;
name: string;
price: number;
}
export class ProductModel {
constructor(public product: Product) {}
getFormattedPrice(): string {
return `₦${this.product.price.toLocaleString()}`;
}
}import { User } from './user';
import { Product } from './product';
export interface Order {
id: number;
user: User;
products: Product[];
total: number;
}
export class OrderModel {
constructor(public order: Order) {}
getOrderSummary(): string {
return `Order #${this.order.id} - ₦${this.order.total}`;
}
}// Re-export everything from each module
export * from './user';
export * from './product';
export * from './order';
// Or re-export specific items
export { User, UserModel } from './user';
export { Product, ProductModel } from './product';
export { Order, OrderModel } from './order';
// Can also add new exports
export const VERSION = "1.0.0";
export function createModels() {
return {
user: UserModel,
product: ProductModel,
order: OrderModel
};
}// Clean imports from single entry point
import {
User,
UserModel,
Product,
ProductModel,
Order,
OrderModel,
VERSION
} from './models';
// Instead of:
// import { User, UserModel } from './models/user';
// import { Product, ProductModel } from './models/product';
// import { Order, OrderModel } from './models/order';
const user: User = {
id: 1,
name: "Yusuf Ibrahim",
email: "yusuf@example.com"
};
const product: Product = {
id: 101,
name: "Laptop",
price: 450000
};
const userModel = new UserModel(user);
const productModel = new ProductModel(product);
console.log(userModel.getFullInfo());
console.log(productModel.getFormattedPrice());
console.log(`Models version: ${VERSION}`);Type-Only Imports and Exports
TypeScript allows importing types without importing runtime code, improving tree-shaking and bundle size.
export interface User {
id: number;
name: string;
email: string;
}
export type UserRole = "admin" | "editor" | "viewer";
export interface AuthConfig {
enabled: boolean;
provider: string;
}
export function createUser(name: string, email: string): User {
return {
id: Date.now(),
name,
email
};
}// Import only types (compile-time only, no runtime code)
import type { User, UserRole, AuthConfig } from './types';
// Import runtime function
import { createUser } from './types';
// Using types
const user: User = createUser("Yusuf", "yusuf@example.com");
const role: UserRole = "admin";
const config: AuthConfig = {
enabled: true,
provider: "oauth"
};
// Import type and value separately
import { type User as UserType, createUser as makeUser } from './types';
const user2: UserType = makeUser("Amina", "amina@example.com");
// Type-only export
export type { User, UserRole }; // Only exports types, not runtime code
export { createUser }; // Exports runtime functionModule Organization Patterns
Feature-Based Organization
src/
├── features/
│ ├── auth/
│ │ ├── index.ts # Re-export everything
│ │ ├── auth.types.ts # Types and interfaces
│ │ ├── auth.service.ts # Business logic
│ │ ├── auth.utils.ts # Helper functions
│ │ └── auth.constants.ts # Constants
│ │
│ ├── users/
│ │ ├── index.ts
│ │ ├── user.types.ts
│ │ ├── user.service.ts
│ │ ├── user.model.ts
│ │ └── user.utils.ts
│ │
│ └── products/
│ ├── index.ts
│ ├── product.types.ts
│ ├── product.service.ts
│ └── product.utils.ts
│
├── shared/
│ ├── types/
│ │ └── index.ts
│ ├── utils/
│ │ └── index.ts
│ └── constants/
│ └── index.ts
│
└── app.tsexport interface LoginCredentials {
email: string;
password: string;
}
export interface AuthToken {
accessToken: string;
refreshToken: string;
expiresIn: number;
}
export interface AuthUser {
id: number;
email: string;
name: string;
role: string;
}import { LoginCredentials, AuthToken, AuthUser } from './auth.types';
export class AuthService {
async login(credentials: LoginCredentials): Promise<AuthToken> {
// Simulate API call
console.log(`Logging in: ${credentials.email}`);
return {
accessToken: "token_" + Date.now(),
refreshToken: "refresh_" + Date.now(),
expiresIn: 3600
};
}
async getCurrentUser(token: string): Promise<AuthUser> {
return {
id: 1,
email: "yusuf@example.com",
name: "Yusuf Ibrahim",
role: "admin"
};
}
logout(): void {
console.log("User logged out");
}
}export const TOKEN_KEY = "auth_token";
export const REFRESH_TOKEN_KEY = "refresh_token";
export const TOKEN_EXPIRY_KEY = "token_expiry";
export const ROLES = {
ADMIN: "admin",
EDITOR: "editor",
VIEWER: "viewer"
} as const;
export type Role = typeof ROLES[keyof typeof ROLES];// Re-export everything from auth feature
export * from './auth.types';
export * from './auth.service';
export * from './auth.constants';
// Can also create convenience exports
export { AuthService as default } from './auth.service';// Clean imports from feature modules
import {
AuthService,
LoginCredentials,
ROLES,
TOKEN_KEY
} from './features/auth';
import { UserService } from './features/users';
import { ProductService } from './features/products';
// Use the services
const authService = new AuthService();
const credentials: LoginCredentials = {
email: "yusuf@example.com",
password: "secret123"
};
authService.login(credentials).then(token => {
console.log("Token:", token.accessToken);
localStorage.setItem(TOKEN_KEY, token.accessToken);
});
console.log("Admin role:", ROLES.ADMIN);Practical Examples
Example 1: API Client Module
export interface ApiResponse<T> {
success: boolean;
data: T;
message?: string;
error?: string;
}
export interface PaginatedResponse<T> {
data: T[];
total: number;
page: number;
pageSize: number;
totalPages: number;
}
export interface RequestConfig {
headers?: Record<string, string>;
params?: Record<string, any>;
timeout?: number;
}import { ApiResponse, RequestConfig } from './types';
export class ApiClient {
constructor(private baseUrl: string) {}
private async request<T>(
endpoint: string,
method: string = "GET",
config?: RequestConfig
): Promise<ApiResponse<T>> {
const url = `${this.baseUrl}${endpoint}`;
try {
const response = await fetch(url, {
method,
headers: {
"Content-Type": "application/json",
...config?.headers
},
signal: config?.timeout
? AbortSignal.timeout(config.timeout)
: undefined
});
const data = await response.json();
return {
success: response.ok,
data,
message: response.ok ? "Success" : "Request failed"
};
} catch (error) {
return {
success: false,
data: null as any,
error: (error as Error).message
};
}
}
async get<T>(endpoint: string, config?: RequestConfig): Promise<ApiResponse<T>> {
return this.request<T>(endpoint, "GET", config);
}
async post<T>(
endpoint: string,
data: any,
config?: RequestConfig
): Promise<ApiResponse<T>> {
return this.request<T>(endpoint, "POST", {
...config,
headers: {
...config?.headers,
"Content-Type": "application/json"
}
});
}
}// Re-export all API-related exports
export * from './types';
export * from './client';
// Create and export default client instance
import { ApiClient } from './client';
export const api = new ApiClient("https://api.example.com");
// Export convenience functions
export async function fetchUsers() {
return api.get("/users");
}
export async function fetchProducts() {
return api.get("/products");
}import {
api,
ApiResponse,
fetchUsers,
fetchProducts
} from './api';
// Use default client
interface User {
id: number;
name: string;
}
api.get<User[]>("/users").then(response => {
if (response.success) {
console.log("Users:", response.data);
}
});
// Use convenience functions
fetchUsers().then(response => {
console.log("Fetched users:", response);
});
fetchProducts().then(response => {
console.log("Fetched products:", response);
});Example 2: Utility Module System
export function capitalize(str: string): string {
return str.charAt(0).toUpperCase() + str.slice(1);
}
export function truncate(str: string, maxLength: number): string {
if (str.length <= maxLength) return str;
return str.slice(0, maxLength - 3) + "...";
}
export function slugify(str: string): string {
return str
.toLowerCase()
.replace(/s+/g, "-")
.replace(/[^a-z0-9-]/g, "");
}export function formatCurrency(amount: number, currency: string = "NGN"): string {
const symbol = currency === "NGN" ? "₦" : currency;
return `${symbol}${amount.toLocaleString()}`;
}
export function clamp(value: number, min: number, max: number): number {
return Math.min(Math.max(value, min), max);
}
export function randomInt(min: number, max: number): number {
return Math.floor(Math.random() * (max - min + 1)) + min;
}export function formatDate(date: Date, format: string = "YYYY-MM-DD"): string {
const year = date.getFullYear();
const month = String(date.getMonth() + 1).padStart(2, "0");
const day = String(date.getDate()).padStart(2, "0");
return format
.replace("YYYY", String(year))
.replace("MM", month)
.replace("DD", day);
}
export function daysFromNow(days: number): Date {
const date = new Date();
date.setDate(date.getDate() + days);
return date;
}
export function isToday(date: Date): boolean {
const today = new Date();
return date.toDateString() === today.toDateString();
}// Re-export all utilities with namespace
export * as StringUtils from './string';
export * as NumberUtils from './number';
export * as DateUtils from './date';
// Or re-export individually
export { capitalize, truncate, slugify } from './string';
export { formatCurrency, clamp, randomInt } from './number';
export { formatDate, daysFromNow, isToday } from './date';// Option 1: Import namespaced utilities
import { StringUtils, NumberUtils, DateUtils } from './utils';
console.log(StringUtils.capitalize("hello")); // "Hello"
console.log(NumberUtils.formatCurrency(50000)); // "₦50,000"
console.log(DateUtils.formatDate(new Date())); // "2025-12-26"
// Option 2: Import specific functions
import { capitalize, formatCurrency, formatDate } from './utils';
console.log(capitalize("typescript")); // "Typescript"
console.log(formatCurrency(100000)); // "₦100,000"
console.log(formatDate(new Date())); // "2025-12-26"
// Option 3: Import all from specific module
import * as string from './utils/string';
console.log(string.capitalize("modules"));
console.log(string.slugify("TypeScript Modules")); // "typescript-modules"Test Your Knowledge
Type Safety Check
Which module export/import is correct?
// File: user.ts
// Option A
export class User {
constructor(public name: string) {}
}
export function createUser(name: string): User {
return new User(name);
}
// File: app.ts (importing from user.ts)
import { User, createUser } from './user';
// Option B
// File: math.ts
const add = (a: number, b: number) => a + b;
export default add;
// File: app.ts
import subtract from './math'; // Wrong name
// Option C
// File: config.ts
export default class Config {
version = "1.0.0";
}
// File: app.ts
import Config from './config';
const config = new Config();
// Option D
// File: utils.ts
export const PI = 3.14159;
export const E = 2.71828;
// File: app.ts
import { PI } from './utils';
import { E as EulerNumber } from './utils';Common TypeScript Error
// File: utils.ts
export function formatPrice(amount: number): string {
return `₦${amount.toLocaleString()}`;
}
function helperFunction(): void {
// Private function - not exported
console.log("Helper");
}
// File: app.ts
import { formatPrice, helperFunction } from './utils';
// Trying to use non-exported function
helperFunction(); // ❌ Error!
// Also trying to import something that doesn't exist
import { calculateTax } from './utils'; // ❌ Error!❌ Module './utils' has no exported member 'helperFunction'. Module './utils' has no exported member 'calculateTax'.
What's Wrong?
You can only import members that are explicitly exported. Private (non-exported) functions and non-existent exports cannot be imported.
// File: utils.ts
export function formatPrice(amount: number): string {
return `₦${amount.toLocaleString()}`;
}
// Export the helper if you need it elsewhere
export function helperFunction(): void {
console.log("Helper");
}
// Add the missing export
export function calculateTax(amount: number, rate: number): number {
return amount * rate;
}
// File: app.ts
import { formatPrice, helperFunction, calculateTax } from './utils';
// ✅ Now all imports work
console.log(formatPrice(50000)); // "₦50,000"
helperFunction(); // "Helper"
console.log(calculateTax(100000, 0.075)); // 7500
// Or only import what you need
import { formatPrice } from './utils';
console.log(formatPrice(25000)); // "₦25,000"Solution: Only import exported members or export the functions you need
Best Practices
- Use named exports for multiple exports - clearer dependencies
- Use default exports for main functionality - one primary export
- Create index.ts barrel files - single entry point for features
- Use type-only imports when importing only types - better tree-shaking
- Organize by feature, not by type - group related code together
- Avoid circular dependencies - refactor shared code
- Keep modules focused - single responsibility
- Use absolute imports with path aliases when possible
// ✅ Good: Feature-based organization
// features/users/index.ts
export * from './user.types';
export * from './user.service';
export { UserService as default } from './user.service';
// ✅ Good: Type-only imports
import type { User, UserRole } from './types';
import { createUser } from './utils';
// ✅ Good: Named exports for utilities
export function formatName(name: string): string {
return name.trim();
}
export function validateEmail(email: string): boolean {
return email.includes("@");
}
// ✅ Good: Default export for main class
export default class UserManager {
// Main functionality
}
// ✅ Good: Re-exporting with namespace
export * as Utils from './utils';
export * as Types from './types';
// ❌ Avoid: Circular dependencies
// file-a.ts
import { B } from './file-b';
export class A {
b = new B();
}
// file-b.ts
import { A } from './file-a'; // Circular!
export class B {
a = new A();
}
// ❌ Avoid: Mixing default and named exports unnecessarily
export default function main() {}
export const helper1 = () => {};
export const helper2 = () => {};
// Better: All named or one clear default
// ❌ Avoid: Too many exports from one file
export const thing1 = 1;
export const thing2 = 2;
// ... 50 more exports
// Better: Split into multiple focused modulesKey Takeaways
- Named exports use
exportkeyword and import with{ } - Default exports use
export defaultand import without{ } - A file can have both default and named exports
- Use
import * as Nameto import all exports as namespace - Type-only imports use
import typefor better tree-shaking - Barrel files (index.ts) create clean entry points
- Re-exporting consolidates multiple modules
- Organize code by feature, not file type
- Avoid circular dependencies between modules
- Modules enable code splitting and reusability
What's Next?
Excellent work! You've mastered TypeScript modules and understand how to organize and structure your code effectively. Next, we'll explore TypeScript with React—how to type React components, props, state, hooks, and build type-safe React applications. This is essential for modern web development!
Get ready to combine TypeScript with React!