Skip to main content

TypeScript Modules

When building Next.js applications, organizing your code effectively is essential for maintainability and scalability. TypeScript modules provide a powerful way to structure your code, enabling you to split your application into manageable pieces that can be imported where needed.

Introduction to TypeScript Modules​

Modules in TypeScript are a way to organize and share code between different parts of your application. They help you:

  • Avoid polluting the global namespace
  • Create reusable components and utilities
  • Manage dependencies between different parts of your code
  • Structure your application logically

In Next.js applications, understanding modules is especially important as the framework relies heavily on modular code organization.

Module Basics​

Exporting from a Module​

In TypeScript, any file containing a top-level import or export is considered a module.

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

export function add(x: number, y: number): number {
return x + y;
}

export function subtract(x: number, y: number): number {
return x - y;
}

You can also use a default export for the main functionality of a module:

typescript
// greeting.ts
function sayHello(name: string): string {
return `Hello, ${name}!`;
}

export default sayHello;

Importing from a Module​

To use exported members from other modules, you need to import them:

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

console.log(PI); // Output: 3.14159
console.log(add(5, 3)); // Output: 8
console.log(sayHello('TypeScript')); // Output: Hello, TypeScript!

Re-exporting​

You can also re-export items from other modules:

typescript
// utils.ts
export { add, subtract } from './math';
export { default as greet } from './greeting';

// Now other files can import from utils

Module Resolution Strategies​

TypeScript supports different module resolution strategies, which determine how the compiler resolves module imports.

Classic Resolution​

The classic resolution strategy is simpler but less powerful:

typescript
import { Component } from './components/Button';
// Looks for:
// - components/Button.ts
// - components/Button.d.ts

Node Resolution​

Node resolution mimics how Node.js resolves modules:

typescript
import { Button } from './components';
// Looks for:
// - components.ts
// - components.d.ts
// - components/index.ts
// - components/index.d.ts
// - components/package.json (if it has a "types" field)

Path Mapping in TypeScript​

In Next.js projects, you'll often use path mapping to create cleaner imports. This is configured in your tsconfig.json:

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

Now you can use these aliases in your imports:

typescript
// Before
import { Button } from '../../components/Button';

// After
import { Button } from '@components/Button';

Named vs Default Exports​

TypeScript modules support both named and default exports, each with their own use cases:

Named Exports​

Named exports are useful when a module exports multiple items:

typescript
// api.ts
export function fetchUsers() { /* ... */ }
export function fetchPosts() { /* ... */ }
export const API_URL = 'https://api.example.com';

Importing named exports:

typescript
import { fetchUsers, API_URL } from './api';
// OR import specific items
import { fetchUsers as getUsers } from './api';
// OR import all as a namespace
import * as API from './api';

API.fetchPosts(); // Using namespace

Default Exports​

Default exports are useful when a module primarily exports a single item:

typescript
// Button.tsx
const Button = ({ text }: { text: string }) => <button>{text}</button>;
export default Button;

Importing default exports:

typescript
import Button from './Button';
import MyButton from './Button'; // can use any name

Dynamic Imports​

Next.js and modern TypeScript support dynamic imports for code splitting:

typescript
// Static import
import { heavyFunction } from './heavyModule';

// Dynamic import
const loadHeavyModule = async () => {
const { heavyFunction } = await import('./heavyModule');
heavyFunction();
};

button.addEventListener('click', loadHeavyModule);

Module Augmentation​

TypeScript allows you to extend existing modules:

typescript
// original-module.d.ts
declare module 'original-module' {
export function existingFunction(): void;
}

// augmentation.ts
declare module 'original-module' {
export function newFunction(): void;
}

// usage.ts
import { existingFunction, newFunction } from 'original-module';

Real-world Example: Creating a Service Module in Next.js​

Let's create a user service module for a Next.js application:

typescript
// services/userService.ts
interface User {
id: number;
name: string;
email: string;
}

export async function getUsers(): Promise<User[]> {
const response = await fetch('/api/users');
return response.json();
}

export async function getUserById(id: number): Promise<User> {
const response = await fetch(`/api/users/${id}`);
return response.json();
}

export async function createUser(user: Omit<User, 'id'>): Promise<User> {
const response = await fetch('/api/users', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(user)
});
return response.json();
}

Using this module in a Next.js page:

typescript
// pages/users.tsx
import { GetServerSideProps } from 'next';
import { getUsers } from '../services/userService';

interface UsersPageProps {
users: Array<{ id: number; name: string; email: string }>;
}

export const getServerSideProps: GetServerSideProps = async () => {
const users = await getUsers();

return {
props: { users }
};
};

export default function UsersPage({ users }: UsersPageProps) {
return (
<div>
<h1>Users</h1>
<ul>
{users.map(user => (
<li key={user.id}>{user.name} ({user.email})</li>
))}
</ul>
</div>
);
}

Barrel Files for Module Organization​

A common pattern in TypeScript projects is to use "barrel" files (index files that re-export from multiple modules):

typescript
// components/index.ts
export { default as Button } from './Button';
export { default as Input } from './Input';
export { default as Card } from './Card';

This allows for cleaner imports:

typescript
// Before
import Button from './components/Button';
import Input from './components/Input';
import Card from './components/Card';

// After
import { Button, Input, Card } from './components';

Summary​

TypeScript modules are a fundamental concept for organizing code in Next.js applications. They allow you to:

  • Split your code into manageable pieces
  • Share functionality between different parts of your application
  • Control what parts of your code are exposed to other modules
  • Create cleaner, more maintainable codebases

By mastering modules, you'll be able to structure your Next.js applications more effectively, leading to code that's easier to maintain and extend.

Additional Resources​

Exercises​

  1. Create a shared utilities module with functions for formatting dates, currency values, and phone numbers.
  2. Build a module for handling authentication in a Next.js app, with functions for login, logout, and checking authentication status.
  3. Create a barrel file that organizes and exports components for a UI library.
  4. Implement path mapping in your tsconfig.json and refactor imports to use aliases.
  5. Create a service module that handles API requests for a blog (posts, comments, users) and use it in a Next.js page.


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