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.
- Problem
A booking system's pricing code is touched by ten engineers; a change in one place silently breaks a refund three files away.
- 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.
- 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.
- Technology
TypeScript (static typing, unions, interfaces), runtime validation (e.g. JSON Schema or schema libraries), unit tests.
- 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.
The problem
2 min- Maya · Product manager37 guests saw a total of €12,036 for a €156 stay. Four were charged €120.36. Nobody got an error.
- You · Engineer at StayHubNobody touched the pricing code. Only the app changed.
- Lina · Senior engineer, your mentorThen guess: what could the app have changed that our code didn't notice?
The big idea
4 minSay 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.
How it works
5 minA 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:
- 1The app sends JSON
App v2.0 sends
"subtotal": "120"(text, in quotes) instead of120. - 2The handler trusts it
req.body.subtotalis whatever arrived: the text "120". No validation, the code that checks outside data before anything uses it. - 3Tax looks right
"120" * 0.3turns text into a number: 36. The bug hides. - 4Plus glues text
"120" + 36joins them: "12036". No crash, no log line. - 5Payment 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.
Watch a decision being made
4 minLina 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.
- 1Follow the value to where it entered
Log
typeof subtotalat each stop: handlerstring, taxnumber, totalstring. 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.
- 2Check at the door
Turn the body from
unknowninto a typed request, or answer 400. Noascasts.tsfunction 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.
- 3Stop trusting the client's price
Drop
subtotalfrom the request. The server loads the rate and computespriceStay(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.
- 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.
In real life
3 minAirbnb: 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.
What goes wrong
3 minTypes are erased at runtime; as BookingRequest only silences the checker.
Validate at the door, then pass typed values inward.
0.1 + 0.2 isn't 0.3; rounding errors add up into wrong invoices.
Integer cents, rounded once, explicitly.
isPaid, isCancelled, isRefunded allow 8 combinations, most nonsense.
One status field with a of allowed states.
Trade-offs
3 min| Option | Fits when | You pay | Main risk |
|---|---|---|---|
| Static typing | Long-lived code, several people, money logic | Effort to describe shapes | False confidence if doors aren't validated |
| Dynamic typing | Prototypes, scripts, tiny teams | Bugs found at runtime | Intent lives in people's heads |
- Karim · CTOThe summer promo engine ships in two weeks. TypeScript now is a luxury. Add tests and move on.
- Lina · Senior engineer, your mentorNot the whole backend: checkout and pricing, about 3,000 of our 40,000 lines. That's where money breaks.
- Karim · CTOEvery type you write is an hour not spent on the promo.
Your job as tech lead
4 minClaude 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.
- 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.
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.
- 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.
- 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.
Check yourself
2 minconst req = body as BookingRequest guarantees at runtime that req.nights is a number.Next episode: the nightly price import took 55 seconds in January. Last night it took 41 minutes, and nobody touched the code.
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.
Technical deep dive
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:
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`;
}
}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.
- 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.
- 2Follow the value to where it entered
Log
typeof subtotalat each stop: handlerstring, taxnumber(36), totalstring. 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.
- 3Check at the door
Turn the body from
unknowninto a typed request, or answer 400. Noascasts.tsfunction 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.
- 4Stop trusting the client's price
Drop
subtotalfrom 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 withpriceStay(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.
- 5Make money a whole number
Store and pass money as integer cents (
12000, not120.00). Convert to euros only for display.Why: Floats round badly (
0.1 + 0.2isn't0.3), and one clear unit removes the euros-versus-cents mix-up that charged €120.36. - 6Prove it with a table of tests
Because
priceStayis pure, test it with plain inputs and outputs:(12000, 1, 30) → 15600,(12000, 3, 30) → 46800. Add a handler test:subtotal: "120"ornights: "3"→ 400.Why: Pure functions test in milliseconds with no database. The 400 test locks the door so the bug can't quietly return.
| Static typing | Dynamic typing | |
|---|---|---|
| When errors appear | Editor / build | At runtime, when that line runs |
| Renaming a field in 300 files | Compiler lists every broken call | Search and tests |
| Speed to first prototype | Slightly slower (declare shapes) | Fastest |
| JSON from outside | Still needs validation | Still needs validation |
| Typical languages | TypeScript, Java, Go, Rust, C# | JavaScript, Python, Ruby, PHP |
Visual diagram
Functional core, imperative shell: validate at the edge, decide in pure functions, act at the edges.
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.
Check your understanding
"12036" instead of 156. What most likely happened?const subtotal = req.body.subtotal; // 120
const tax = computeTax(subtotal); // 36
const total = subtotal + tax;Practice ladder
Trace a booking through untyped code
Debugging Warm-upStayHub'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?
Finish Lina's refactor of the cancellation fee
Code PracticeThis 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.
- Find the decision: the only real decision is the fee.
- Write the pure function: ___ (its name, inputs and output, and what makes it pure)
- Shell: load the booking, call the pure function with
new Date(), write the result. - Test it: ___ (at least three cases)
A partner sends room availability
Design PracticeA 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.
TypeScript for the internal reports script?
Decision StretchMaya 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.
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.