Skip to content

APPLICATION DEVELOPMENT · LESSON 14 OF 24

Modules and project structure

Split responsibilities across files with explicit imports.

  • ES modules
  • import type
  • NodeNext
  • exports

Your runnable checkpoint

Extract into a new practice folder. Each download includes complete source, package.json, tsconfig.json and an offline lesson. Use Node.js 22 or 24. Each checkpoint is independent.

Install with npm install. Run npm run check, then npm run build. Terminal lessons use npm start; browser lessons use npm run serve and http://127.0.0.1:4173.

1. Understand the concepts

Each module exposes only what it exports. Separate domain types, business functions and application entry points so changes have a clear home. Use named exports for clear dependencies.

The checkpoints use type: module in package.json and NodeNext module resolution. Relative imports refer to the emitted .js filename even when the source is .ts. TypeScript resolves that import to the source during checking; Node resolves the generated file at runtime.

import type removes a purely type-level dependency from emitted JavaScript. verbatimModuleSyntax makes the distinction explicit. A value imported using import type cannot be called at runtime.

Node, browsers and bundlers resolve modules differently. Do not copy a bundler-specific path alias into a plain Node app and expect Node to resolve it. Keep this example’s relative paths and file extensions intact.

2. Build and run

These are complete files. Preserve the paths shown below, including the src directory. The package and compiler configuration are included at the end.

src/main.ts

src/main.ts
import { total } from "./cart.js";
import type { Line } from "./models.js";
const cart: Line[] = [{ name: "Notebook", price: 50, quantity: 3 }];
console.log(total(cart));

src/models.ts

src/models.ts
export type Line = { name: string; price: number; quantity: number };

src/cart.ts

src/cart.ts
import type { Line } from "./models.js";
export function total(lines: readonly Line[]): number {
  return lines.reduce((sum, line) => sum + line.price * line.quantity, 0);
}

package.json

package.json
{
  "name": "edupoly-typescript-modules-and-project-structure",
  "private": true,
  "version": "1.0.0",
  "type": "module",
  "scripts": {
    "check": "tsc --noEmit",
    "build": "tsc",
    "start": "node dist/main.js"
  },
  "devDependencies": {
    "typescript": "5.9.3"
  }
}

tsconfig.json

tsconfig.json
{
  "compilerOptions": {
    "target": "ES2022",
    "module": "NodeNext",
    "moduleResolution": "NodeNext",
    "lib": [
      "ES2022",
      "DOM"
    ],
    "strict": true,
    "noUncheckedIndexedAccess": true,
    "exactOptionalPropertyTypes": true,
    "noEmitOnError": true,
    "verbatimModuleSyntax": true,
    "rootDir": "src",
    "outDir": "dist",
    "sourceMap": true,
    "types": [],
    "moduleDetection": "force"
  },
  "include": [
    "src/**/*"
  ]
}

3. Trace the code and check the result

  1. models.ts exports a type and emits no useful runtime model constructor.
  2. cart.ts owns the calculation and imports only its required type.
  3. main.ts composes the modules and prints the result.

Expected result

150

4. Practice independently

Add a second cart line in main.ts without changing the calculation module.

Show the explanation

Add { name: "Pen", price: 10, quantity: 2 }. The total becomes 170; the function already accepts any readonly sequence of Line values.

When something goes wrong

If tsc is missing, run npm install in the folder containing package.json. If the output looks old, rebuild before running. Read the first compiler diagnostic and check the indicated field or argument. For module errors, preserve the .js extensions in imports. Browser examples must run through the included local server, not directly as TypeScript files.

Read the related official TypeScript documentation ↗