ES Modules in TypeScript
TypeScript uses ES module syntax (`import`/`export`) as its native module system. Understanding named exports, default exports, re-exports, and the `import type` syntax helps you write modular, tree-shakable code. The compiled output format is controlled by `tsconfig.json`'s `module` option.
ES Modules — Patterns & Best Practices
// ── Named exports — the default choice ───────────────────────
// src/types/domain.ts
export interface Task { id: number; title: string; status: TaskStatus; }
export type TaskStatus = "todo" | "in-progress" | "done" | "archived";
export const DEFAULT_PAGE_SIZE = 20;
// ── Named imports ──────────────────────────────────────────────
import { Task, TaskStatus, DEFAULT_PAGE_SIZE } from "./types/domain";
// ── Rename on import ──────────────────────────────────────────
import { Task as TaskDomain } from "./types/domain";
// ── Namespace import ──────────────────────────────────────────
import * as Domain from "./types/domain";
const task: Domain.Task = { /* ... */ };
// ── Default export — use sparingly ────────────────────────────
// src/services/taskService.ts
export default class TaskService { /* ... */ }
// Importing default:
import TaskService from "./services/taskService";
// Problem: caller can rename it — "TaskService" is not enforced.
// Prefer named exports for most things.
// ── Re-export (barrel) ────────────────────────────────────────
// src/index.ts — public API barrel
export { Task, TaskStatus } from "./types/domain";
export { TaskService } from "./services/taskService";
export { TaskRepository } from "./repositories/taskRepository";
// Callers import from one place: import { Task, TaskService } from "@/";
// ── import type — type-only imports (erased at compile time) ──
// ALWAYS use for importing types. Keeps JS output clean.
import type { Task } from "./types/domain";
import type { User } from "./types/user";
// These imports are 100% erased from the output JS.
// Mixed imports:
import { taskId, type Task as T } from "./types/domain";
// taskId is a value (kept in JS output)
// Task is a type (erased from JS output)
// ── Dynamic import — lazy loading ─────────────────────────────
async function loadHeavyModule() {
const { HeavyParser } = await import("./utils/heavyParser");
return HeavyParser.parse(data);
}Common Mistakes
- Using default exports everywhere — default exports are harder to rename-consistently across a large codebase and don't benefit from tree-shaking as cleanly. Prefer named exports.
- Not using `import type` for type imports — without `import type`, bundlers may try to process type-only imports. Always use `import type { Foo }` for types. With `verbatimModuleSyntax: true` in tsconfig, TypeScript enforces this.
- Circular imports — if module A imports from B, and B imports from A, the module initialisation order is undefined. Extract shared types to a third module C that both A and B import from.
Tip
Tip
Practice ES Modules in TypeScript in small, isolated examples before integrating into larger projects. Breaking concepts into small experiments builds genuine understanding faster than reading alone.
Use ESM for all new projects. CJS is legacy.
Practice Task
Note
Practice Task — (1) Write a working example of ES Modules in TypeScript from scratch without looking at notes. (2) Modify it to handle an edge case (empty input, null value, or error state). (3) Share your solution in the Priygop community for feedback.
Quick Quiz
Common Mistake
Warning
A common mistake with ES Modules in TypeScript is skipping edge case testing — empty inputs, null values, and unexpected data types. Always validate boundary conditions to write robust, production-ready typescript code.
Key Takeaways
- TypeScript uses ES module syntax (`import`/`export`) as its native module system.
- Using default exports everywhere — default exports are harder to rename-consistently across a large codebase and don't benefit from tree-shaking as cleanly. Prefer named exports.
- Not using `import type` for type imports — without `import type`, bundlers may try to process type-only imports. Always use `import type { Foo }` for types. With `verbatimModuleSyntax: true` in tsconfig, TypeScript enforces this.
- Circular imports — if module A imports from B, and B imports from A, the module initialisation order is undefined. Extract shared types to a third module C that both A and B import from.