Modules, imports and exports
A real project is many files, and modules are how they connect: each file exports what it offers and
imports what it needs. TypeScript uses the standard ES module system (the import/export you know
from JavaScript) and adds one important twist — type-only imports. This lesson is modules the way you
will actually organise a TypeScript codebase.
Exports and imports
Every .ts file is a module. You export what other files may use, and import what you need:
// money.ts
export function formatRupees(paise: number): string {
return `₹${(paise / 100).toFixed(2)}`;
}
export const CURRENCY = "INR";
export interface Money {
paise: number;
}
// order.ts
import { formatRupees, CURRENCY, type Money } from "./money";
const price: Money = { paise: 45000 };
console.log(formatRupees(price.paise)); // ₹450.00
export before a declaration makes it available; import { ... } from "./money" brings specific things
in by name (a named import). The path "./money" is relative — ./ means "in the same folder",
../ means "up one" — and you omit the .ts extension. TypeScript checks that what you import actually
exists and is exported — importing a name that is not exported is a compile error, and it knows the types
of everything you import.
Default exports — and why to prefer named
A module can have one default export, imported without braces and under any name:
// logger.ts
export default class Logger { /* ... */ }
// app.ts
import Logger from "./logger"; // no braces; you choose the name
import MyLogger from "./logger"; // same thing, different local name — allowed
Default exports are common (React components often use them), but named exports are usually better, for real reasons:
- Consistent names. A named export must be imported by its exact name (
import { Logger }), so the same thing is called the same everywhere — easier to search, refactor, and read. A default export can be renamed at each import, which fragments the name. - Better autocomplete and refactoring. Editors handle named exports more reliably (auto-import, rename-across-files).
- Multiple exports scale. A file with several exports needs named exports anyway; mixing one default with several named is inconsistent.
The guidance many teams adopt: prefer named exports; use a default only where a convention requires it (a React component file, a framework's page module). This course leans that way — named exports, so names stay consistent across the codebase.
Type-only imports — the TypeScript twist
Here is the TypeScript-specific feature. Because types are erased at compile time (they do not exist at
runtime), an import that is used only as a type can be marked type, telling TypeScript "this import
disappears in the output":
import { type Money, formatRupees } from "./money"; // Money is type-only; formatRupees is a value
import type { User, Order } from "./models"; // the whole import is type-only
import type { User } (or import { type User }) imports User only for type-checking — it is
completely removed from the compiled JavaScript, because a type has no runtime existence. Why does this
matter?
- Clarity. It documents that an import is used only for types, not for runtime values.
- Correctness with bundlers. Some build setups can mis-handle a value import that is actually only a
type;
import typeremoves ambiguity and prevents accidental runtime dependencies (importing a type should not force the module to be loaded at runtime). - The
verbatimModuleSyntax/isolatedModulesoptions (real-projects module) sometimes requireimport typefor type-only imports, so it is a habit worth forming.
The rule: when you import something you use only as a type — an interface, a type alias — use import type (or the inline type modifier). For things used as runtime values (functions, classes, constants),
use a normal import. Mixing both in one statement (import { type Money, formatRupees }) is fine and
common.
Re-exports and barrel files
You can re-export from another module, which is how "barrel" files (an index.ts that gathers a folder's
exports) work:
// models/index.ts — a barrel
export { User } from "./user";
export { Order } from "./order";
export type { Money } from "./money";
// elsewhere — import everything from one place:
import { User, Order, type Money } from "./models";
export { X } from "./x" re-exports X so it is available from this module too. A barrel index.ts
gathers a folder's public exports into one import path — convenient, though large barrels can slow builds
and cause circular-import issues, so use them judiciously (per feature, not one giant barrel for the whole
app).
import for side effects, and dynamic import
Two more forms you will meet:
import "./setup"; // run the module for its side effects, import nothing
const mod = await import("./heavy"); // DYNAMIC import — load a module lazily, at runtime
import "./setup" runs a module's top-level code without importing any binding — for a module that
registers something globally. await import("./heavy") is a dynamic import — it loads a module lazily
at runtime and returns a promise, used for code-splitting (loading a heavy feature only when needed).
Dynamic import returns a typed module object, so mod.someExport is checked.
Check your work
What each .ts file is. A module — it exports what it offers and imports what it needs.
Named imports and relative paths. import { X } from "./file" — by exact name, relative path, no
.ts extension; TypeScript checks the import exists.
Default exports, and why prefer named. A default is imported without braces under any name; named exports keep names consistent, refactor better, and scale — prefer named except where a convention requires a default.
Type-only imports. import type { X } (or inline type) imports something used only as a type,
removed entirely from the compiled JS — for clarity, bundler correctness, and required by some strict
options.
When to use import type. For interfaces/type aliases used only as types; normal import for runtime
values (functions, classes, constants).
Re-exports and barrels. export { X } from "./x" re-exports; a barrel index.ts gathers a folder's
exports — use judiciously.
Side-effect and dynamic imports. import "./x" runs a module for effects; await import("./x")
loads lazily at runtime for code-splitting.
Practice
- Create two files; export a function, a constant, and an interface from one, and import them into the other. Confirm the types come through.
- Import a name that is not exported and read the error.
- Add a default export and import it under two different names. Then rewrite it as a named export and note the consistency.
- Import an interface with
import typeand confirm (in the compiled JS) that it is removed. - Mix a type-only and a value import in one statement (
import { type X, y }). - Create a barrel
index.tsre-exporting two modules, and import from it. - Use a dynamic
await import(...)and confirm the returned module's exports are typed.
Official documentation
- TypeScript — Modules — Imports, exports, and module syntax.
- TypeScript — Type-Only Imports and Exports —
import type. - TypeScript — verbatimModuleSyntax — Why type-only imports matter for bundlers.
Next: decorators — what they are, ahead of NestJS.
Stuck on this lesson?
Being stuck is part of it — but being stuck alone for three days is not. Our internship programme pairs this curriculum with code review and one-to-one help from working developers, and it is free.
About the internship