A TypeScript module is a file that exports or imports something. Modules keep code organized by giving each file a clear responsibility and by making dependencies explicit.
Without modules, large projects quickly become difficult to navigate. With modules, utilities, models, services, components, and API clients can live in separate files while still working together through imports and exports.
TypeScript modules use JavaScript module syntax. TypeScript adds type checking, type-only imports, and compiler settings that control how modules are resolved and emitted.
export function add(a: number, b: number): number {
return a + b;
}
export function multiply(a: number, b: number): number {
return a * b;
}
export const TAX_RATE = 0.18;
Named exports are explicit and easy to search. A file can export many named values, and importers choose exactly what they need.
Named exports are usually a strong default for utilities, constants, shared models, service functions, and feature helpers because refactors are clearer and accidental name changes are easier to catch.
import { add, TAX_RATE } from "./math";
export function calculateInvoice(subtotal: number): number {
const tax = subtotal * TAX_RATE;
return add(subtotal, tax);
}
A default export represents the main value from a file. Default exports are common for page components, one primary class, or a module that naturally exposes one main thing.
Use default exports intentionally. In larger shared utility files, named exports are often easier to maintain because every imported name is tied to the exported name.
| Export Style | Common Use |
|---|---|
| Named export | Utilities, constants, models, multiple public values. |
| Default export | One main component, class, or service from a file. |
| Mixed exports | Use carefully so the file API stays obvious. |
export default class UserService {
findName(id: number): string {
return id === 1 ? "Admin" : "Guest";
}
}
// app.ts
import UserService from "./UserService";
const service = new UserService();
console.log(service.findName(1));
import type imports only type information. The import is erased from compiled JavaScript because interfaces and type aliases do not exist at runtime.
Using import type makes intent clear and can avoid build problems in projects that enforce isolated modules or strict module syntax. It also helps developers see which dependencies are runtime dependencies and which are only for checking.
import { calculateInvoice } from "./billing";
import type { User } from "./types";
const user: User = {
id: 1,
name: "Admin",
};
console.log(user.name, calculateInvoice(1000));
A feature folder may contain several files. An index.ts file can re-export the public pieces so other parts of the app import from one stable path.
Use this carefully. Re-exporting everything from everywhere can create confusing dependency graphs. Export only the API that other folders should depend on.
export type { User, UserRole } from "./user.types";
export { createUserService } from "./user.service";
export { validateUserInput } from "./user.validation";
Path aliases make imports shorter and more stable. Instead of importing from ../../../services/users, a project might use @/services/users.
Aliases are usually configured in tsconfig.json and must also be understood by the bundler or runtime. TypeScript can type-check an alias, but the tool that runs the code must know how to resolve it too.
import { createUserService } from "@/features/users";
import type { User } from "@/features/users";
export async function loadProfile(id: number): Promise<User | null> {
const service = createUserService();
return service.findById(id);
}
Good modules have clear ownership. A feature module should expose what other parts of the app need and keep internal helpers private. This prevents unrelated code from depending on implementation details.
If a shared folder becomes a dumping ground, it becomes hard to understand. Prefer specific shared modules such as date, money, http, or validation over vague folders like misc.
| Module Choice | Effect |
|---|---|
| Small focused file | Easy to test and reuse. |
| Feature index file | Clean public imports for a folder. |
| Deep private import | Can create fragile dependencies. |
| Circular import | Can cause confusing runtime behavior. |
Most module problems come from unclear ownership. A shared folder should not become a dumping ground, and feature modules should avoid reaching deeply into each other’s private files.
Keep imports pointed at public entry points when possible. If a file is imported from many unrelated places, it may belong in a shared utility package or it may need a clearer abstraction.
import type is erased from JavaScript output.
Explore 500+ free tutorials across 20+ languages and frameworks.