On this page
Modules and namespaces
Modules in TypeScript
TypeScript's module system is built on top of the ECMAScript module (ESM) specification. Every .ts file with a top-level import or export statement is a module — it has its own scope and must explicitly export what it wants to share. A file without any import or export is a script that runs in the global scope (avoid this pattern in modern projects).
Named exports and imports
// geometry.ts
export function circleArea(radius: number): number {
return Math.PI * radius ** 2;
}
export function rectArea(width: number, height: number): number {
return width * height;
}
export const PI_APPROX = 3.14159;// main.ts
import { circleArea, PI_APPROX } from "./geometry.js";
console.log(circleArea(5)); // 78.539...
console.log(PI_APPROX); // 3.14159Default exports
A module can have one default export. Default exports are convenient for components, classes, and configuration objects:
// logger.ts
export default class Logger {
private prefix: string;
constructor(prefix: string) {
this.prefix = prefix;
}
info(message: string): void {
console.log(`[${this.prefix}] INFO: ${message}`);
}
error(message: string): void {
console.error(`[${this.prefix}] ERROR: ${message}`);
}
}// app.ts
import Logger from "./logger.js"; // any name is valid for default imports
const log = new Logger("App");
log.info("Starting...");In practice, many style guides prefer named exports over defaults because they are easier to rename, tree-shake, and autocomplete.
Type-only imports and exports
TypeScript provides import type and export type syntax for imports that exist only at the type level. The emitted JavaScript will not contain these imports:
// models.ts
export type User = {
id: number;
name: string;
role: "admin" | "user";
};
export interface Session {
token: string;
userId: number;
expiresAt: number;
}// service.ts
import type { User, Session } from "./models.js";
function getUser(session: Session): User | undefined {
// Implementation would go here
return undefined;
}The import type statement guarantees that the import is erased completely during compilation. If you mix types and values in a single import, TypeScript 4.5+ lets you mark individual specifiers:
import { type User, createUser, type Session } from "./models.js";Module resolution
TypeScript supports several module resolution strategies configured via tsconfig.json:
| Strategy | Use case |
|---|---|
Node |
CommonJS projects (legacy) |
NodeNext |
Node.js ESM projects |
Bundler |
Vite, webpack, esbuild — the bundler handles resolution |
Classic |
Legacy TypeScript (avoid) |
For modern projects using a bundler, set "moduleResolution": "Bundler". For Node.js ESM, use "NodeNext".
Path aliases
Deep relative imports like ../../../services/auth are brittle. Configure path aliases in tsconfig.json to use clean absolute-style paths:
{
"compilerOptions": {
"baseUrl": ".",
"paths": {
"@app/*": ["src/app/*"],
"@core/*": ["src/core/*"],
"@shared/*": ["src/shared/*"],
"@env": ["src/environments/environment.ts"]
}
}
}Now you can write:
import { AuthService } from "@core/services/auth.js";
import { Button } from "@shared/components/button.js";Important: Path aliases are a TypeScript-only feature. Your bundler or runtime also needs to resolve them. Vite uses vite.config.ts → resolve.alias; Node.js needs a package exports map or a loader.
Barrel files
A barrel is an index.ts file that re-exports everything from a directory, creating a single public API entry point:
// src/validators/index.ts
export { EmailValidator } from "./email-validator.js";
export { PasswordValidator } from "./password-validator.js";
export { PhoneValidator } from "./phone-validator.js";
export type { ValidationResult } from "./types.js";Consumers import from the barrel rather than hunting for individual file paths:
import { EmailValidator, PhoneValidator } from "@validators/index.js";Declaration files (.d.ts)
A .d.ts file contains only type declarations — no runtime code. TypeScript generates them automatically when you set "declaration": true in tsconfig.json. They describe the public API of a compiled library so other TypeScript projects can consume it with full type safety.
You also write .d.ts files manually to add types to JavaScript libraries that do not ship their own:
// src/types/analytics.d.ts
declare module "my-analytics" {
export interface TrackOptions {
event: string;
properties?: Record<string, unknown>;
userId?: string;
}
export function track(options: TrackOptions): void;
export function identify(userId: string, traits?: Record<string, unknown>): void;
}DefinitelyTyped and @types
For popular JavaScript libraries, the community maintains type declarations in the DefinitelyTyped repository, published as @types/* packages:
npm install --save-dev @types/node @types/express @types/lodashTypeScript automatically picks up these packages — no import needed. They augment the module's types globally.
Namespaces (legacy)
Namespaces were TypeScript's pre-ESM module system. They merge declarations under a single identifier and can span multiple files. In modern projects you should prefer ES modules:
// Avoid in new code — use ES modules instead
namespace Validation {
export interface StringValidator {
isAcceptable(s: string): boolean;
}
export class LettersOnlyValidator implements StringValidator {
isAcceptable(s: string): boolean {
return /^[A-Za-z]+$/.test(s);
}
}
}
const v = new Validation.LettersOnlyValidator();The only valid modern use case for namespaces is augmenting global declaration files or organizing related ambient declarations in .d.ts files.
Practice
- ES module structure: Create a small module
string-utils.tsthat exports three named functions:capitalize,slugify, andtruncate. Import and use all three in amain.tsfile. Use.jsextensions in the imports. - Type-only import: Define a
UserProfileinterface intypes.ts. Inuser-service.ts, import it withimport typeand use it as the return type of afetchProfile(id: number): Promise<UserProfile>function. - Path aliases: Configure
@utilsand@modelspath aliases in atsconfig.json. Write the barrelindex.tsfor each directory and update the imports in your service files to use the aliases. - Ambient module: Write a
.d.tsdeclaration file for a fictional"date-fns-lite"module that exports two functions:format(date: Date, pattern: string): stringandaddDays(date: Date, days: number): Date.
Next lesson: migrating JavaScript to TypeScript — a practical, incremental approach.
// --- math.ts ---
// Named exports
export function add(a: number, b: number): number {
return a + b;
}
export function multiply(a: number, b: number): number {
return a * b;
}
// Re-export from another module
export { PI } from "./constants.js";
// --- types.ts ---
// Type-only export — erased at compile time, never included in JS output
export type { User } from "./models.js";
export type UserId = number;
// --- main.ts ---
// Named import
import { add, multiply } from "./math.js";
// Type-only import — signals this import is for types only
import type { UserId } from "./types.js";
// Namespace import (import everything as an object)
import * as MathUtils from "./math.js";
console.log(add(2, 3)); // 5
console.log(MathUtils.multiply(4, 5)); // 20
const id: UserId = 42; // Only used as a type — no runtime import
{
"compilerOptions": {
"baseUrl": ".",
"paths": {
"@models/*": ["src/models/*"],
"@utils/*": ["src/utils/*"],
"@services/*": ["src/services/*"]
}
}
}
Sign in to track your progress