Skip to content
Episode 9 · The checkout that said €12,036

Types, functions and control flow

Tuesday, 14:20: 37 guests on the new StayHub app saw a checkout total of €12,036 for a €156 stay. Nothing crashed, no error was logged, and the code hasn't changed in months.

Variables, types and functions aren't just syntax. They're your first tools for catching bugs before a guest ever sees them.

30 min foundationStatic vs dynamic typingModelling data with types (unions, interfaces)Pure functions and side effectsNever trust the client
  1. Problem

    A booking system's pricing code is touched by ten engineers; a change in one place silently breaks a refund three files away.

  2. Why the simple system fails

    With untyped data and functions that read and write global state, nothing tells you what a value can be or what a call might change. Bugs surface in production, at checkout.

  3. Engineering concept

    Describe the shape of data with types, keep decisions in small pure functions, push side effects to the edges, and validate everything that crosses a boundary.

  4. Technology

    TypeScript (static typing, unions, interfaces), runtime validation (e.g. JSON Schema or schema libraries), unit tests.

  5. Trade-offs

    Static types cost upfront ceremony and some flexibility; they pay back as code and teams grow. Types are erased at runtime, so they never replace input validation.

01

The problem

2 min
Tuesday, 14:20. The new StayHub app went live at 13:05.
  1. Maya · Product manager
    37 guests saw a total of €12,036 for a €156 stay. Four were charged €120.36. Nobody got an error.
  2. You · Engineer at StayHub
    Nobody touched the pricing code. Only the app changed.
  3. Lina · Senior engineer, your mentor
    Then guess: what could the app have changed that our code didn't notice?
02

The big idea

4 min
Where did €12,036 come from?
The big idea

Say what shape your data has, and check data from outside at the door.

The maths was right. What was missing: nothing said the subtotal must be a number, and nothing checked the JSON when it arrived.

03

How it works

5 min

A type is what kind of value something is: a number, text, an object with certain fields. StayHub's backend is plain JavaScript, which uses : types are checked only as each line runs. Follow the value:

What really happened to the subtotal
  1. 1
    The app sends JSON

    App v2.0 sends "subtotal": "120" (text, in quotes) instead of 120.

  2. 2
    The handler trusts it

    req.body.subtotal is whatever arrived: the text "120". No validation, the code that checks outside data before anything uses it.

  3. 3
    Tax looks right

    "120" * 0.3 turns text into a number: 36. The bug hides.

  4. 4
    Plus glues text

    "120" + 36 joins them: "12036". No crash, no log line.

  5. 5
    Payment reads it

    The payment code reads "12036" as cents: €120.36 charged to four guests.

(for example ) catches mistakes inside our code before it runs. But types are erased at runtime, so JSON from outside still needs a real check. And decisions like a price belong in a : same inputs, same output, no database, clock or email. like charging a card stay few and at the edges.

04

Watch a decision being made

4 min
Worked example

Lina fixes the €12,036 checkout

The app sends the subtotal as text, and checkout shows €12,036 instead of €156. Make this whole family of bugs unable to reach a guest again.

  1. 1Follow the value to where it entered

    Log typeof subtotal at each stop: handler string, tax number, total string. The wrong type came in with the JSON.

    Why: The line that looks wrong isn't where it started. Fix the source, or it reappears.

  2. 2Check at the door

    Turn the body from unknown into a typed request, or answer 400. No as casts.

    ts
    function parseCheckout(x: unknown): CheckoutRequest {
      if (typeof x !== "object" || x === null) throw new BadRequest("body must be an object");
      const { hotelId, nights } = x as Record<string, unknown>;
      if (typeof hotelId !== "string") throw new BadRequest("hotelId required");
      if (!Number.isInteger(nights) || (nights as number) < 1) throw new BadRequest("nights >= 1");
      return { hotelId, nights: nights as number };
    }

    Why: Only real code can vouch for outside data. One check at the door lets everything inside trust the types.

  3. 3Stop trusting the client's price

    Drop subtotal from the request. The server loads the rate and computes priceStay(rateCents, nights, 30), a pure function, in integer cents.

    Why: A phone's price can be wrong or faked. Whole cents remove the euros-versus-cents mix-up behind €120.36.

  4. 4Prove it with a table of tests

    (12000, 1, 30) → 15600, (12000, 3, 30) → 46800. Handler test: nights: "3" → 400.

    Why: Pure functions test in milliseconds; the 400 test locks the door.

The whole thing in one line: Types guard the inside, validation guards the door. The price is decided by a pure server-side function in whole cents: €12,036 became €156, and bad input gets a 400.
05

In real life

3 min
Two well-known teams, two choices

Airbnb: move to TypeScript

  • Has publicly described migrating its large JavaScript codebase to TypeScript
  • Many engineers touch the same code; the compiler catches breakage across files
  • Cost: a long migration and types to maintain

Worth it when many people change a big codebase for years.

Basecamp: keep plain JavaScript

  • Its creator publicly announced removing TypeScript from the Turbo library in 2023
  • Small team, less ceremony; type mistakes rely on tests

Workable when a small team owns all the code.

06

What goes wrong

3 min
Trusting TypeScript types for request bodies

Types are erased at runtime; as BookingRequest only silences the checker.

Instead

Validate at the door, then pass typed values inward.

Storing money as floating-point numbers

0.1 + 0.2 isn't 0.3; rounding errors add up into wrong invoices.

Instead

Integer cents, rounded once, explicitly.

Modelling states with independent booleans

isPaid, isCancelled, isRefunded allow 8 combinations, most nonsense.

Instead

One status field with a of allowed states.

07

Trade-offs

3 min
OptionFits whenYou payMain risk
Static typingLong-lived code, several people, money logicEffort to describe shapesFalse confidence if doors aren't validated
Dynamic typingPrototypes, scripts, tiny teamsBugs found at runtimeIntent lives in people's heads
Monday, 10:00. Planning meeting.
  1. Karim · CTO
    The summer promo engine ships in two weeks. TypeScript now is a luxury. Add tests and move on.
  2. Lina · Senior engineer, your mentor
    Not the whole backend: checkout and pricing, about 3,000 of our 40,000 lines. That's where money breaks.
  3. Karim · CTO
    Every type you write is an hour not spent on the promo.
08

Your job as tech lead

4 min

Claude Code can write the code for this. You still own the decisions, and you have to know what good looks like to accept or reject its work.

Decisions you own
  • How money is represented everywhere: whole cents as integers, never strings or floats.
  • Where incoming data gets checked: once, at the edge, for every field an app or partner sends; a bad request gets a clear 400.
  • Whether StayHub moves to TypeScript, and in what order. It's a team decision with a cost, not something the agent starts alone.
Tell Claude Code
Mobile app v2.0 sends subtotal as the string "120"; checkout showed "12036" instead of 156, and 4 guests were charged €120.36. Our backend is plain JavaScript. At the start of the checkout handler, validate that subtotal and guests are whole numbers in a sensible range, else return 400 with a clear message. Move price and tax into a pure function taking numbers only, with table tests including the "120" string case. Don't touch payment code. Plan first.
Check before you accept
  • Claude sends the bad request (subtotal "120") and shows you a 400, not a total.
  • The test table includes the string case, a missing field, a negative number and 120 → 156.
  • The pricing function has no database call, clock or network inside.
Red flags
  • A Number() deep inside the pricing code, so the next bad field slips through.
  • TypeScript types added and the input called "safe now".
  • Money stored as a float (120.36) or a string.
09

Check yourself

2 min
Q1/3Reasoning
In TypeScript, const req = body as BookingRequest guarantees at runtime that req.nights is a number.
How confident are you?

Next episode: the nightly price import took 55 seconds in January. Last night it took 41 minutes, and nobody touched the code.

Finished?

Mark it complete to update your skill map.

Go deeper (optional)You don't need this to understand the lesson. Open it when a project needs the details: Technical deep dive, Visual diagram, Analogy, Check your understanding, Practice ladder, Explain it yourself.
10

Technical deep dive

Check at the door, decide in the middle

Here's the shape I want: say what states a booking can be in, keep the price calculation pure, and check outside data once, at the door. A lets the compiler force every case to be handled:

ts
type Cents = number; // integer cents: never floats for money

type Booking =
  | { status: "pending"; id: string; totalCents: Cents }
  | { status: "paid"; id: string; totalCents: Cents; paidAt: Date }
  | { status: "cancelled"; id: string; refundCents: Cents };

// Pure: depends only on its inputs, no I/O, no clock, no mutation.
function priceStay(nightlyCents: Cents, nights: number, taxPct: number): Cents {
  const gross = nightlyCents * nights;
  return gross + Math.round((gross * taxPct) / 100);
}

function label(b: Booking): string {
  switch (b.status) {
    case "pending":   return "Awaiting payment";
    case "paid":      return `Paid on ${b.paidAt.toISOString().slice(0, 10)}`; // paidAt exists only here
    case "cancelled": return `Refunded ${b.refundCents / 100} EUR`;
  }
}
Types say what we mean; a union makes impossible states fail to compile.
Worked example

Lina fixes the €12,036 checkout

The new app sends the subtotal as text, and checkout shows €12,036 instead of €156. Fix it so this whole family of bugs can't reach a guest again.

  1. 1Reproduce with the real input

    Copy one real request body from the logs and run the checkout with it: { "hotelId": "h42", "nights": 1, "subtotal": "120" }. Total: "12036".

    Why: A bug you can repeat on demand is half fixed. It also proves the input, not the pricing code, is the trigger.

  2. 2Follow the value to where it entered

    Log typeof subtotal at each stop: handler string, tax number (36), total string. The wrong type came in with the JSON.

    Why: The line that looks wrong (the total) isn't where the problem started. Fix it at the source, or it reappears elsewhere.

  3. 3Check at the door

    Turn the body from unknown into a typed request, or answer 400. No as casts.

    ts
    function parseCheckout(x: unknown): CheckoutRequest {
      if (typeof x !== "object" || x === null) throw new BadRequest("body must be an object");
      const { hotelId, nights } = x as Record<string, unknown>;
      if (typeof hotelId !== "string") throw new BadRequest("hotelId required");
      if (!Number.isInteger(nights) || (nights as number) < 1) throw new BadRequest("nights >= 1");
      return { hotelId, nights: nights as number };
    }

    Why: Types are erased at runtime, so only real code can vouch for outside data. One check at the door lets everything inside trust the types.

  4. 4Stop trusting the client's price

    Drop subtotal from the request: the checkout path built for app v2.0 brought back the €7.20 mistake by taking the price from the phone. The server loads the nightly rate from the database and computes the price itself with priceStay(rateCents, nights, 30).

    Why: A price sent by a phone can be wrong or faked. Prices are a decision the server must make from its own data.

  5. 5Make money a whole number

    Store and pass money as integer cents (12000, not 120.00). Convert to euros only for display.

    Why: Floats round badly (0.1 + 0.2 isn't 0.3), and one clear unit removes the euros-versus-cents mix-up that charged €120.36.

  6. 6Prove it with a table of tests

    Because priceStay is pure, test it with plain inputs and outputs: (12000, 1, 30) → 15600, (12000, 3, 30) → 46800. Add a handler test: subtotal: "120" or nights: "3" → 400.

    Why: Pure functions test in milliseconds with no database. The 400 test locks the door so the bug can't quietly return.

The whole thing in one line: Lina found that text entered at the door, added validation there, moved the price decision into a pure server-side function in integer cents, and locked it in with tests: €12,036 became €156 and bad input now gets a 400.
Static typingDynamic typing
When errors appearEditor / buildAt runtime, when that line runs
Renaming a field in 300 filesCompiler lists every broken callSearch and tests
Speed to first prototypeSlightly slower (declare shapes)Fastest
JSON from outsideStill needs validationStill needs validation
Typical languagesTypeScript, Java, Go, Rust, C#JavaScript, Python, Ruby, PHP
Many dynamic languages now have optional checkers (Python type hints), blurring the line.
11

Visual diagram

Click a component

Functional core, imperative shell: validate at the edge, decide in pure functions, act at the edges.

12

Analogy

  • Paperwork at a hotelA form with labelled boxesIn codeA type
  • Paperwork at a hotelA clerk checking forms at the doorIn codeStatic type checking
  • Paperwork at a hotelFinding the bad form while processing itIn codeDynamic typing errors
  • Paperwork at a hotelA calculatorIn codeA pure function
  • Paperwork at a hotelA courier: call twice, two parcelsIn codeA function with side effects
  • Paperwork at a hotelReception checking posted-in formsIn codeValidating data from outside

Where the analogy breaks: the clerk only checks forms written inside your building. JSON arriving from the internet skips the clerk entirely, because types disappear when the program runs. So you must check it yourself at the door.

13

Check your understanding

Q1/3Debugging
A plain-JavaScript checkout shows a total of "12036" instead of 156. What most likely happened?
const subtotal = req.body.subtotal; // 120
const tax = computeTax(subtotal);    // 36
const total = subtotal + tax;
How confident are you?
14

Practice ladder

Step 1 · Trace

Trace a booking through untyped code

Debugging Warm-up

StayHub's plain-JavaScript handler:

const nights = req.body.nights;          // app sends "2"
const rate = hotel.rateCents;            // 9000 (a number)
const extraBed = req.body.extraBed ? 1500 : 0;  // app sends "false"
const total = rate * nights + extraBed;

The app sends { "nights": "2", "extraBed": "false" }. Trace each line: what type and value does each variable get, and what is total? Is the guest charged correctly?

Step 2 · Complete

Finish Lina's refactor of the cancellation fee

Code Practice

This function is hard to test because it reads the clock and writes to the database:

async function cancel(bookingId) {
  const b = await db.bookings.find(bookingId);
  const hoursLeft = (b.checkIn - Date.now()) / 3600000;
  const feeCents = hoursLeft < 24 ? b.totalCents : 0;
  await db.bookings.update(bookingId, { status: "cancelled", feeCents });
}

Lina's plan has gaps. Fill in steps 2 and 4.

  1. Find the decision: the only real decision is the fee.
  2. Write the pure function: ___ (its name, inputs and output, and what makes it pure)
  3. Shell: load the booking, call the pure function with new Date(), write the result.
  4. Test it: ___ (at least three cases)
Step 3 · Solve

A partner sends room availability

Design Practice

A hotel partner's system will send StayHub updates as JSON:

{ "hotelId": "h42", "date": "2026-11-03", "roomsLeft": 3, "priceCents": 12000 }

Their docs say all fields are always present. Design how StayHub receives this: what you check at the door, what type you use inside, which parts are pure, and what happens with a bad message. Write the type and the list of checks.

Step 4 · Decide

TypeScript for the internal reports script?

Decision Stretch

Maya wants a script that reads last month's bookings and prints a CSV of revenue per hotel for the finance team. It's 150 lines, one engineer will write it this week, and it runs once a month on a laptop. The rest of the checkout code is moving to TypeScript.

Karim: "Do we write it in TypeScript too, for consistency?"

Write your decision in this form: "We chose X over Y because…, accepting the risk of…". Say what you'd do about money in the script either way.

15

Explain it yourself

Explain to a teammate why TypeScript alone didn't stop a bad request body from reaching the pricing code, and how you'd structure the code so it can't happen again.

0 words