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.
// 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:
// 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:
// 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:
// 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:
import { Component } from './components/Button';
// Looks for:
// - components/Button.ts
// - components/Button.d.ts
Node Resolution
Node resolution mimics how Node.js resolves modules:
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
:
{
"compilerOptions": {
"baseUrl": ".",
"paths": {
"@components/*": ["components/*"],
"@utils/*": ["utils/*"]
}
}
}
Now you can use these aliases in your imports:
// 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:
// api.ts
export function fetchUsers() { /* ... */ }
export function fetchPosts() { /* ... */ }
export const API_URL = 'https://api.example.com';
Importing named exports:
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:
// Button.tsx
const Button = ({ text }: { text: string }) => <button>{text}</button>;
export default Button;
Importing default exports:
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:
// 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:
// 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:
// 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:
// 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):
// 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:
// 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
- Create a shared utilities module with functions for formatting dates, currency values, and phone numbers.
- Build a module for handling authentication in a Next.js app, with functions for login, logout, and checking authentication status.
- Create a barrel file that organizes and exports components for a UI library.
- Implement path mapping in your
tsconfig.json
and refactor imports to use aliases. - 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 [email protected]. I’d greatly appreciate your feedback! :)