Skip to main content

TypeScript Modules

Introduction​

As your TypeScript applications grow in size and complexity, organizing your code becomes essential for maintainability. TypeScript modules provide a powerful way to structure your code, encapsulate functionality, and control what parts of your code are visible to other parts of your application.

In this tutorial, we'll explore TypeScript modules - how they work, how to import and export code between files, and best practices for using modules effectively in your TypeScript projects.

What Are TypeScript Modules?​

A module in TypeScript is a file that contains code - variables, functions, classes, interfaces, or types - that can be exported to be used in other files. Any file containing a top-level import or export is considered a module.

Modules help you:

  • Organize code: Break large applications into smaller, manageable pieces
  • Encapsulate code: Hide implementation details and expose only what's necessary
  • Reuse code: Share functionality across different parts of your application
  • Control dependencies: Explicitly declare what your code needs from other modules

Module Formats in TypeScript​

TypeScript supports several module formats:

  1. ES Modules (ESM) - Modern JavaScript standard, using import and export statements
  2. CommonJS - The Node.js module system, using require() and module.exports
  3. AMD - Used mainly in browser applications with RequireJS
  4. UMD - Universal Module Definition that works across multiple environments
  5. System - SystemJS dynamic module loader

For most modern applications, ES Modules are recommended and will be our focus in this tutorial.

Exporting from a Module​

Let's look at the different ways to export code from a TypeScript module.

Named Exports​

You can export specific variables, functions, classes, interfaces, or types using the export keyword:

typescript
// math.ts
export const PI = 3.14159;

export function add(a: number, b: number): number {
return a + b;
}

export function subtract(a: number, b: number): number {
return a - b;
}

export interface Shape {
area(): number;
}

export type Point = {
x: number;
y: number;
};

You can also declare items first and export them later:

typescript
// math.ts
const PI = 3.14159;
function add(a: number, b: number): number {
return a + b;
}
function subtract(a: number, b: number): number {
return a - b;
}

// Export multiple items at once
export { PI, add, subtract };

Default Exports​

Each module can have one default export, which makes it easier to import:

typescript
// calculator.ts
class Calculator {
add(a: number, b: number): number {
return a + b;
}

subtract(a: number, b: number): number {
return a - b;
}
}

export default Calculator;

Mixing Default and Named Exports​

You can combine default and named exports in the same file:

typescript
// math.ts
export const PI = 3.14159;

export function add(a: number, b: number): number {
return a + b;
}

export default class MathUtils {
static square(x: number): number {
return x * x;
}

static cube(x: number): number {
return x * x * x;
}
}

Importing from a Module​

Now let's see how to import the exported items from our modules.

Importing Named Exports​

typescript
// app.ts
import { PI, add, subtract } from './math';

console.log(PI); // 3.14159
console.log(add(5, 3)); // 8
console.log(subtract(10, 4)); // 6

You can also rename imports to avoid naming conflicts:

typescript
// app.ts
import { PI as MathPI, add as mathAdd } from './math';

console.log(MathPI); // 3.14159
console.log(mathAdd(5, 3)); // 8

Importing Default Exports​

typescript
// app.ts
import Calculator from './calculator';

const calc = new Calculator();
console.log(calc.add(5, 3)); // 8

Importing Both Default and Named Exports​

typescript
// app.ts
import MathUtils, { PI, add } from './math';

console.log(PI); // 3.14159
console.log(add(5, 3)); // 8
console.log(MathUtils.square(4)); // 16

Importing All Exports​

You can import all exports from a module into a single namespace:

typescript
// app.ts
import * as MathModule from './math';

console.log(MathModule.PI); // 3.14159
console.log(MathModule.add(5, 3)); // 8

// If there's a default export, it will be available as default
const utils = new MathModule.default();

Module Resolution Strategies​

TypeScript supports different module resolution strategies that determine how the compiler locates modules:

  1. Classic: A simplified resolution strategy used mainly for backward compatibility
  2. Node: Mirrors how Node.js resolves modules (recommended for Node.js applications)
  3. Node16 or NodeNext: Modern resolution strategies for newer Node.js versions with ESM support

You can specify the resolution strategy in your tsconfig.json:

json
{
"compilerOptions": {
"moduleResolution": "node",
// other options...
}
}

Path Mapping​

TypeScript allows you to define path aliases in your tsconfig.json to make imports cleaner and more maintainable:

json
{
"compilerOptions": {
"baseUrl": ".",
"paths": {
"@utils/*": ["src/utils/*"],
"@models/*": ["src/models/*"]
}
}
}

This allows you to import like this:

typescript
// Instead of relative paths like this:
import { formatDate } from '../../utils/date';

// You can use:
import { formatDate } from '@utils/date';

Real-World Example: Building a Todo Application​

Let's see how modules can be used in a real-world scenario by building a simple todo application with modular architecture:

Model Module​

typescript
// src/models/todo.ts
export interface Todo {
id: number;
title: string;
completed: boolean;
createdAt: Date;
}

export type TodoStatus = 'all' | 'active' | 'completed';

Service Module​

typescript
// src/services/todo-service.ts
import { Todo } from '../models/todo';

export default class TodoService {
private todos: Todo[] = [];
private nextId = 1;

addTodo(title: string): Todo {
const todo: Todo = {
id: this.nextId++,
title,
completed: false,
createdAt: new Date()
};

this.todos.push(todo);
return todo;
}

getTodos(): Todo[] {
return [...this.todos]; // Return a copy
}

toggleTodo(id: number): Todo | undefined {
const todo = this.todos.find(todo => todo.id === id);
if (todo) {
todo.completed = !todo.completed;
}
return todo;
}

deleteTodo(id: number): boolean {
const initialLength = this.todos.length;
this.todos = this.todos.filter(todo => todo.id !== id);
return initialLength > this.todos.length;
}
}

Utilities Module​

typescript
// src/utils/format-utils.ts
export function formatDate(date: Date): string {
return date.toLocaleDateString('en-US', {
year: 'numeric',
month: 'short',
day: 'numeric'
});
}

export function truncateText(text: string, maxLength: number = 50): string {
if (text.length <= maxLength) return text;
return text.slice(0, maxLength) + '...';
}

UI Module​

typescript
// src/ui/todo-list.ts
import { Todo, TodoStatus } from '../models/todo';
import TodoService from '../services/todo-service';
import { formatDate, truncateText } from '../utils/format-utils';

export class TodoList {
private todoService: TodoService;
private currentFilter: TodoStatus = 'all';

constructor(todoService: TodoService) {
this.todoService = todoService;
}

addTodo(title: string): void {
if (!title.trim()) return;

const todo = this.todoService.addTodo(title);
console.log(`Added new todo: "${truncateText(todo.title, 20)}" on ${formatDate(todo.createdAt)}`);
this.renderTodos();
}

toggleTodo(id: number): void {
const todo = this.todoService.toggleTodo(id);
if (todo) {
console.log(`Todo "${truncateText(todo.title, 20)}" marked as ${todo.completed ? 'completed' : 'active'}`);
this.renderTodos();
}
}

deleteTodo(id: number): void {
if (this.todoService.deleteTodo(id)) {
console.log(`Todo deleted successfully`);
this.renderTodos();
}
}

setFilter(filter: TodoStatus): void {
this.currentFilter = filter;
this.renderTodos();
}

private renderTodos(): void {
const allTodos = this.todoService.getTodos();

// Filter todos based on current filter
let filteredTodos: Todo[];
if (this.currentFilter === 'active') {
filteredTodos = allTodos.filter(todo => !todo.completed);
} else if (this.currentFilter === 'completed') {
filteredTodos = allTodos.filter(todo => todo.completed);
} else {
filteredTodos = allTodos;
}

console.log(`------ ${this.currentFilter.toUpperCase()} TODOS ------`);
filteredTodos.forEach(todo => {
const status = todo.completed ? '✓' : '○';
console.log(`${status} [${todo.id}] ${todo.title} (${formatDate(todo.createdAt)})`);
});
console.log(`----------------------------`);
}
}

Main Application​

typescript
// src/app.ts
import TodoService from './services/todo-service';
import { TodoList } from './ui/todo-list';

// Create instances
const todoService = new TodoService();
const todoList = new TodoList(todoService);

// Add some todos
todoList.addTodo('Learn TypeScript Modules');
todoList.addTodo('Build a modular application');
todoList.addTodo('Write documentation');

// Toggle the first todo as completed
todoList.toggleTodo(1);

// Show active todos only
todoList.setFilter('active');

// Delete a todo
todoList.deleteTodo(3);

// Show all todos
todoList.setFilter('all');

In this example, we've separated our application into logical modules:

  • Models contain data structures
  • Services handle business logic
  • Utils provide helper functions
  • UI components handle rendering and user interaction

The main app file simply imports and uses these modules without needing to know their implementation details.

Module Bundlers​

When building web applications with TypeScript and modules, you'll typically use a module bundler like:

  1. Webpack - A powerful and flexible bundler
  2. Rollup - Great for libraries and smaller applications
  3. Parcel - Zero configuration bundler
  4. esbuild - Extremely fast bundler

These bundlers allow you to:

  • Bundle multiple modules into a single file
  • Tree-shake to remove unused code
  • Split code into chunks for lazy loading
  • Process and transform assets

Dynamic Imports​

TypeScript supports dynamic imports for loading modules on demand:

typescript
// Regular static import
import { someFunction } from './some-module';

// Dynamic import (returns a Promise)
async function loadModule() {
const module = await import('./some-module');
module.someFunction();
}

This is useful for:

  • Lazy loading modules when needed
  • Reducing initial bundle size
  • Loading modules conditionally

Best Practices for TypeScript Modules​

  1. Prefer named exports for most cases as they provide better clarity and are easier to refactor.
  2. Use default exports sparingly and mainly for modules that export a single primary element.
  3. Export interfaces and types to share type definitions across your application.
  4. Keep modules focused on a single responsibility.
  5. Avoid circular dependencies between modules.
  6. Use barrel files (index.ts) to simplify imports from complex module structures:
typescript
// src/utils/index.ts
export * from './date-utils';
export * from './string-utils';
export * from './number-utils';

// Now you can import everything with:
// import { formatDate, truncateText, formatNumber } from './utils';
  1. Organize modules by feature rather than by type (models, services, components) for large applications.

Summary​

TypeScript modules provide a powerful way to organize your code into reusable, maintainable units. In this tutorial, we've covered:

  • Exporting and importing code in TypeScript modules
  • Different types of exports (named, default)
  • Module resolution strategies
  • Path mapping for cleaner imports
  • A real-world example demonstrating modular architecture
  • Dynamic imports for lazy loading
  • Best practices for using modules effectively

By mastering TypeScript modules, you'll be able to create well-structured applications that are easier to maintain, test, and scale.

Additional Resources​

Exercises​

  1. Create a simple calculator module with functions for add, subtract, multiply, and divide. Then import and use these functions in another file.

  2. Convert an existing non-modular TypeScript application into one that uses modules.

  3. Create a barrel file (index.ts) that re-exports multiple related modules to simplify imports.

  4. Implement a feature that uses dynamic imports to lazy load a module only when it's needed.

  5. Set up path aliases in a tsconfig.json file to use absolute imports instead of relative ones.



If you spot any mistakes on this website, please let me know at feedback@compilenrun.com. I’d greatly appreciate your feedback! :)