Skip to content

Lesson 9 of 1014 min

Practical application patterns

A few small patterns keep real code readable as it grows - modules, pure functions, dependency injection and keeping I/O at the edges.

In this lesson

  • Organise code into modules with clear exports
  • Separate pure logic from input and output
  • Pass dependencies in to make code testable

Modules: one job per file#

Each file is a module. export makes something available; import uses it. Exporting only what others need keeps the rest private and free to change.

money.ts
// money.tsexport function formatRupiah(amount: number): string {  return `Rp ${amount.toLocaleString("id-ID")}`;}// checkout.tsimport { formatRupiah } from "./money";console.log(formatRupiah(150000)); // "Rp 150.000"

Pure functions at the core#

A pure function returns the same output for the same input and changes nothing outside itself. Pure code is easy to test and reason about. Keep business rules pure and push effects (network, storage, time, randomness) to the edges.

discount.ts
type Order = { subtotal: number; isMember: boolean };function discountFor(order: Order): number {1  const rate = order.isMember ? 0.1 : 0;  return Math.round(order.subtotal * rate);}async function checkout(order: Order): Promise<void> {2  const discount = discountFor(order);  const response = await fetch("https://api.example.com/orders", {    method: "POST",    headers: { "content-type": "application/json" },    body: JSON.stringify({ ...order, discount }),  });  if (!response.ok) throw new Error(`Checkout failed: HTTP ${response.status}`);}

Pure: no network, no clock, no globals. Test it with plain inputs and expected outputs.

The effectful shell: it calls the pure core, then does the I/O.

Pass dependencies in#

If a function reaches for a global (the clock, fetch, a database client), tests have to fake that global. Passing the dependency as a parameter makes the function honest about what it needs.

greeting.ts
function greeting(now: Date = new Date()): string {1  const hour = now.getHours();  if (hour < 11) return "Selamat pagi";  if (hour < 15) return "Selamat siang";  if (hour < 19) return "Selamat sore";  return "Selamat malam";}console.log(greeting(new Date(2026, 0, 1, 8))); // "Selamat pagi"2

Production code calls greeting(); tests pass a fixed date. No mocking library needed.

Months are zero-based in Date, so 0 is January.

Interfaces at the boundary#

Describe what a dependency must do, not which library provides it. Swapping storage, or using an in-memory version in tests, then becomes trivial.

store.ts
interface KeyValueStore {  get(key: string): Promise<string | undefined>;  set(key: string, value: string): Promise<void>;}class MemoryStore implements KeyValueStore {  private readonly data = new Map<string, string>();  async get(key: string) {    return this.data.get(key);  }  async set(key: string, value: string) {    this.data.set(key, value);  }}async function rememberTheme(store: KeyValueStore, theme: "light" | "dark") {  await store.set("theme", theme);}await rememberTheme(new MemoryStore(), "dark");

How much structure?

Interfaces and injected dependencies cost a little ceremony. For a 50-line script, skip them. Once code has tests, several callers or more than one environment (browser, server, test), the seams pay for themselves quickly.

ExerciseMake it testable

This function is hard to test because it reads the clock itself. Rewrite it so a test can control the date.

exercise.ts
function isWeekend(): boolean {  const day = new Date().getDay();  return day === 0 || day === 6;}
Show solution
solution.ts
function isWeekend(now: Date = new Date()): boolean {  const day = now.getDay();  return day === 0 || day === 6;}console.log(isWeekend(new Date(2026, 9, 31))); // true: 31 October 2026 is a Saturday

Recap#

  • Modules export a small surface and hide the rest.
  • Keep rules pure; do I/O in a thin outer layer.
  • Inject dependencies and code against interfaces so tests stay simple.