RizTech Academy logo
RizTech Academy
Classes, Modules and DecoratorsLesson 3 of 420 min

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 type removes ambiguity and prevents accidental runtime dependencies (importing a type should not force the module to be loaded at runtime).
  • The verbatimModuleSyntax / isolatedModules options (real-projects module) sometimes require import type for 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

  1. Create two files; export a function, a constant, and an interface from one, and import them into the other. Confirm the types come through.
  2. Import a name that is not exported and read the error.
  3. Add a default export and import it under two different names. Then rewrite it as a named export and note the consistency.
  4. Import an interface with import type and confirm (in the compiled JS) that it is removed.
  5. Mix a type-only and a value import in one statement (import { type X, y }).
  6. Create a barrel index.ts re-exporting two modules, and import from it.
  7. Use a dynamic await import(...) and confirm the returned module's exports are typed.

Official documentation

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