An educational pure functional programming library in TypeScript
2
fork

Configure Feed

Select the types of activity you want to include in your feed.

Add Beaver's Big System story trilogy teaching Branded Types, Typestate, ADTs, and Tracked Arrays

+2412
+16
docs/guides/stories/README.md
··· 39 39 40 40 --- 41 41 42 + ### [Beaver's Big System](./beavers-big-system/) 43 + 44 + A sequel trilogy about building maintainable maintenance software. 45 + 46 + | Part | Title | Concepts | Status | 47 + |------|----------------------------------------------------------------------------|--------------------------------|-----------| 48 + | 1 | [The Mixup](./beavers-big-system/01-the-mixup.md) | Branded Types + Typestate | Available | 49 + | 2 | [The Categorization](./beavers-big-system/02-the-categorization.md) | ADTs + Pattern Matching | Available | 50 + | 3 | [The Queue](./beavers-big-system/03-the-queue.md) | Tracked Arrays + Refinements | Available | 51 + 52 + **Setup:** After the election, Beaver builds "The System" — a work order tracker. But strings everywhere lead to chaos: mixed-up IDs, invalid states, unsorted queues. Owl guides the rebuild. 53 + 54 + **Same characters** continue their journey, learning compile-time safety through infrastructure problems. 55 + 56 + --- 57 + 42 58 ## See Also 43 59 44 60 - [Tutorial](../tutorial/) — Step-by-step guide building a complete application
+412
docs/guides/stories/beavers-big-system/01-the-mixup.md
··· 1 + # The Mixup 2 + 3 + *Part 1 of Beaver's Big System* 4 + 5 + > In which the animals learn that not all strings are created equal, 6 + > and some transitions should be impossible. 7 + 8 + --- 9 + 10 + The forest had never been busier. After the election, President Deer announced an ambitious infrastructure program: repair the old dam, reinforce the eastern bridge, clear the overgrown paths. The animals needed a way to track it all. 11 + 12 + "I'll build a system!" Beaver declared at the town meeting. "I'm good at building things." 13 + 14 + And Beaver was. Within three days, the Work Order Tracker was ready. A beautiful wooden board with slots for leaf-cards, each representing a job to be done. 15 + 16 + "It's simple," Beaver explained proudly. "Each work order has an ID, a target — that's what we're fixing — and a status. When work starts, move the card to 'In Progress'. When it's done, move it to 'Done'." 17 + 18 + Fox examined a card. "What's `dam-1` mean?" 19 + 20 + "That's the ID. I just number them as they come in." 21 + 22 + "And `dam-north`?" 23 + 24 + "That's the target — the north dam." 25 + 26 + "They're both strings that start with 'dam'," Fox observed. "That seems... fragile." 27 + 28 + Beaver waved a paw dismissively. "It's fine. We all know what's what." 29 + 30 + --- 31 + 32 + It was not fine. 33 + 34 + The first incident happened on a Thursday. Fox, tired after a long day, grabbed the wrong card. He moved `dam-north` to the "Done" column — but he'd meant to update work order `dam-1`, which was about reinforcing the *south* dam. 35 + 36 + "Why does this say the north dam is fixed?" Badger asked the next morning. "I was just there. The spillway is still cracked." 37 + 38 + Beaver checked the board. "Oh no. Fox marked the wrong thing." 39 + 40 + "They're labeled almost identically!" Fox protested. "dam-north, dam-1 — how was I supposed to know one's a *location* and one's an *order ID*?" 41 + 42 + The second incident was worse. Rabbit, updating her burrow inspection report, typed the status as `in_progerss`. Nobody noticed for two days, because the system accepted any string at all. 43 + 44 + "We have three work orders stuck in a status that doesn't exist," Owl observed, studying the board. "The system thinks they're valid." 45 + 46 + "Can't we just check for typos?" Beaver asked. 47 + 48 + "We could," said Owl. "But what about `In-Progress`, `in progress`, `InProgress`? How many variations do we check against?" 49 + 50 + The third incident was the strangest. Badger found a work order marked "Done" with a completion time — but no start time. Someone had dragged it straight from "Pending" to "Done", skipping "In Progress" entirely. 51 + 52 + "How is that possible?" Rabbit wrung her paws. "You can't finish work that never started!" 53 + 54 + "The system doesn't know that," Beaver said quietly. "It lets you put any status anywhere." 55 + 56 + --- 57 + 58 + That evening, Owl called a meeting. 59 + 60 + "I've been studying the system," she said. "The problem isn't the design — it's the *types*. Everything is a string." 61 + 62 + She scratched marks in the dirt: 63 + 64 + ```typescript 65 + // Beaver's original system 66 + type WorkOrder = { 67 + id: string // "dam-1", "wo-47" 68 + targetId: string // "dam-north", "bridge-east" 69 + status: string // "pending", "in-progress", "done" 70 + } 71 + ``` 72 + 73 + "Three different kinds of values, all represented the same way. The compiler can't tell them apart. So when Fox swaps an order ID with a target ID—" 74 + 75 + "The system just accepts it," Fox finished. "Like it's perfectly normal." 76 + 77 + "Exactly." 78 + 79 + --- 80 + 81 + "Here's the first fix." Owl made a fresh leaf. "We create *distinct* types — even though they're still strings underneath." 82 + 83 + ```typescript 84 + import { type Branded, brand } from "purus-ts" 85 + 86 + // Each ID type is distinct — you can't mix them up 87 + type WorkOrderId = Branded<string, "WorkOrderId"> 88 + type DamId = Branded<string, "DamId"> 89 + type BridgeId = Branded<string, "BridgeId"> 90 + type PathId = Branded<string, "PathId"> 91 + type BurrowId = Branded<string, "BurrowId"> 92 + ``` 93 + 94 + "They're called *branded types*," Owl explained. "A `DamId` is a string with a brand. A `BridgeId` is a string with a *different* brand. The compiler treats them as completely different types." 95 + 96 + "Show me," said Fox, still skeptical. 97 + 98 + Owl wrote more: 99 + 100 + ```typescript 101 + // Creating branded IDs 102 + const damNorth: DamId = brand("dam-north") 103 + const workOrder1: WorkOrderId = brand("wo-1") 104 + 105 + // This compiles — same types 106 + const validAssignment: DamId = damNorth 107 + 108 + // This FAILS to compile — different brands! 109 + // const invalidAssignment: DamId = workOrder1 110 + // Error: Type 'WorkOrderId' is not assignable to type 'DamId' 111 + ``` 112 + 113 + "So my mistake last Thursday—" Fox began. 114 + 115 + "Would have been caught immediately. The compiler would refuse to accept a `WorkOrderId` where a `DamId` was expected." 116 + 117 + Beaver's eyes widened. "We can make it impossible to mix them up?" 118 + 119 + "At compile time. Before the code ever runs." 120 + 121 + --- 122 + 123 + "But that's only half the problem," said Rabbit. "What about the status? The typos? The impossible transitions?" 124 + 125 + Owl nodded. "For that, we need *typestate*." 126 + 127 + She drew a diagram: 128 + 129 + ``` 130 + start() complete() 131 + Pending ─────────► InProgress ─────────► Completed 132 + │ │ 133 + └──────────────── cannot skip ─────────────────┘ 134 + ``` 135 + 136 + "A work order isn't just 'pending' or 'done'. It's *in a state*. And certain operations are only valid in certain states." 137 + 138 + ```typescript 139 + // Each state is a distinct type 140 + type PendingWork = { 141 + readonly _state: "Pending" 142 + readonly id: WorkOrderId 143 + readonly targetId: DamId | BridgeId | PathId | BurrowId 144 + readonly createdAt: number 145 + } 146 + 147 + type InProgressWork = { 148 + readonly _state: "InProgress" 149 + readonly id: WorkOrderId 150 + readonly targetId: DamId | BridgeId | PathId | BurrowId 151 + readonly createdAt: number 152 + readonly startedAt: number // Must have a start time 153 + } 154 + 155 + type CompletedWork = { 156 + readonly _state: "Completed" 157 + readonly id: WorkOrderId 158 + readonly targetId: DamId | BridgeId | PathId | BurrowId 159 + readonly createdAt: number 160 + readonly startedAt: number 161 + readonly completedAt: number // Must have a completion time 162 + } 163 + 164 + // The union of all possible states 165 + type WorkOrder = PendingWork | InProgressWork | CompletedWork 166 + ``` 167 + 168 + "Each state type has exactly the fields that make sense for that state," Owl continued. "Pending work doesn't have a `startedAt`. Completed work must have both times." 169 + 170 + --- 171 + 172 + "Now watch what happens with the transitions." Owl wrote the functions: 173 + 174 + ```typescript 175 + // Can only start work that's pending 176 + const startWork = (work: PendingWork): InProgressWork => ({ 177 + ...work, 178 + _state: "InProgress", 179 + startedAt: Date.now(), 180 + }) 181 + 182 + // Can only complete work that's in progress 183 + const completeWork = (work: InProgressWork): CompletedWork => ({ 184 + ...work, 185 + _state: "Completed", 186 + completedAt: Date.now(), 187 + }) 188 + ``` 189 + 190 + "The types enforce the rules. You *can't* call `completeWork` on `PendingWork`—" 191 + 192 + "Because it expects `InProgressWork`," Beaver said, catching on. 193 + 194 + "So Badger's impossible work order—" Rabbit started. 195 + 196 + "Could never exist. The only way to get a `CompletedWork` is through `completeWork`, which requires `InProgressWork`, which requires `startWork` on `PendingWork`. The chain is unbreakable." 197 + 198 + --- 199 + 200 + Fox leaned forward. "What about the typos? The `in_progerss` problem?" 201 + 202 + "Gone." Owl smiled. "There is no `status: string` anymore. The state is encoded in the *type*. You can't misspell a type." 203 + 204 + ```typescript 205 + // This compiles - proper state 206 + const pending: PendingWork = { 207 + _state: "Pending", 208 + id: brand("wo-1"), 209 + targetId: brand("dam-north"), 210 + createdAt: Date.now(), 211 + } 212 + 213 + // This FAILS - not a valid state value 214 + // const invalid: PendingWork = { 215 + // _state: "Pendnig", // Typo! 216 + // ... 217 + // } 218 + // Error: Type '"Pendnig"' is not assignable to type '"Pending"' 219 + ``` 220 + 221 + "The compiler checks the literal string against the expected value. `"Pendnig"` is not `"Pending"`, so it's rejected." 222 + 223 + --- 224 + 225 + Beaver had been quiet, thinking. "Let me rebuild the system." 226 + 227 + ```typescript 228 + import { type Branded, brand, match, pipe } from "purus-ts" 229 + 230 + // ================================================================= 231 + // Branded IDs — Each infrastructure type has its own ID type 232 + // ================================================================= 233 + 234 + type WorkOrderId = Branded<string, "WorkOrderId"> 235 + type DamId = Branded<string, "DamId"> 236 + type BridgeId = Branded<string, "BridgeId"> 237 + 238 + const workOrderId = (id: string): WorkOrderId => brand(id) 239 + const damId = (id: string): DamId => brand(id) 240 + const bridgeId = (id: string): BridgeId => brand(id) 241 + 242 + // ================================================================= 243 + // Typestate — Work orders progress through defined states 244 + // ================================================================= 245 + 246 + type TargetId = DamId | BridgeId 247 + 248 + type PendingWork = { 249 + readonly _state: "Pending" 250 + readonly id: WorkOrderId 251 + readonly targetId: TargetId 252 + readonly description: string 253 + readonly createdAt: number 254 + } 255 + 256 + type InProgressWork = { 257 + readonly _state: "InProgress" 258 + readonly id: WorkOrderId 259 + readonly targetId: TargetId 260 + readonly description: string 261 + readonly createdAt: number 262 + readonly startedAt: number 263 + readonly assignee: string 264 + } 265 + 266 + type CompletedWork = { 267 + readonly _state: "Completed" 268 + readonly id: WorkOrderId 269 + readonly targetId: TargetId 270 + readonly description: string 271 + readonly createdAt: number 272 + readonly startedAt: number 273 + readonly completedAt: number 274 + readonly assignee: string 275 + readonly notes: string 276 + } 277 + 278 + type WorkOrder = PendingWork | InProgressWork | CompletedWork 279 + 280 + // ================================================================= 281 + // State Transitions — Each function enforces valid progression 282 + // ================================================================= 283 + 284 + const createWorkOrder = ( 285 + id: WorkOrderId, 286 + targetId: TargetId, 287 + description: string, 288 + ): PendingWork => ({ 289 + _state: "Pending", 290 + id, 291 + targetId, 292 + description, 293 + createdAt: Date.now(), 294 + }) 295 + 296 + const startWork = ( 297 + work: PendingWork, 298 + assignee: string, 299 + ): InProgressWork => ({ 300 + ...work, 301 + _state: "InProgress", 302 + startedAt: Date.now(), 303 + assignee, 304 + }) 305 + 306 + const completeWork = ( 307 + work: InProgressWork, 308 + notes: string, 309 + ): CompletedWork => ({ 310 + ...work, 311 + _state: "Completed", 312 + completedAt: Date.now(), 313 + notes, 314 + }) 315 + ``` 316 + 317 + "Beautiful," said Owl. "Now let's see it catch errors." 318 + 319 + --- 320 + 321 + Beaver demonstrated: 322 + 323 + ```typescript 324 + // Create some work orders 325 + const damWork = createWorkOrder( 326 + workOrderId("wo-1"), 327 + damId("dam-north"), 328 + "Repair spillway crack", 329 + ) 330 + 331 + const bridgeWork = createWorkOrder( 332 + workOrderId("wo-2"), 333 + bridgeId("bridge-east"), 334 + "Reinforce support beams", 335 + ) 336 + 337 + // This WORKS - proper progression 338 + const started = startWork(damWork, "Beaver") 339 + const completed = completeWork(started, "Spillway sealed with river clay") 340 + 341 + // This FAILS - can't skip states! 342 + // const badComplete = completeWork(damWork, "Done!") 343 + // Error: Argument of type 'PendingWork' is not assignable 344 + // to parameter of type 'InProgressWork' 345 + 346 + // This FAILS - can't mix ID types! 347 + // const confused = createWorkOrder( 348 + // damId("dam-north"), // Oops, used DamId instead of WorkOrderId 349 + // damId("dam-north"), 350 + // "Fix something", 351 + // ) 352 + // Error: Argument of type 'DamId' is not assignable 353 + // to parameter of type 'WorkOrderId' 354 + ``` 355 + 356 + "Every mistake I made last month," said Fox slowly, "would be caught before the code even runs." 357 + 358 + "That's the power of the type system," said Owl. "It's not about more checking. It's about making invalid states *unrepresentable*." 359 + 360 + --- 361 + 362 + The moon was rising over the forest. The animals gathered their leaves and prepared to leave. 363 + 364 + "This is good," said Badger. "But I notice we're treating all work orders the same. Dam repairs and bridge work and path clearing — they're all just `WorkOrder`." 365 + 366 + "Is that a problem?" asked Beaver. 367 + 368 + "Dam repairs need water level checks," Badger said. "Bridge work needs weight capacity assessments. Path clearing needs different equipment. They're not really the same thing at all." 369 + 370 + Owl gathered her notes. "Then tomorrow, we'll talk about *discriminated unions*. Different types of work, each with their own requirements — all handled safely." 371 + 372 + "More types," Fox muttered, but there was no complaint in his voice. "I'm starting to see why." 373 + 374 + Rabbit had stopped wringing her paws. For the first time in days, she looked calm. 375 + 376 + "No more mystery statuses," she said. "No more impossible transitions. The system knows what's valid." 377 + 378 + Beaver looked at the wooden board with its leaf-cards. Tomorrow, they'd build something better. Something that couldn't fail in the ways the old system had failed. 379 + 380 + "Same time tomorrow?" Beaver asked. 381 + 382 + The animals nodded and dispersed into the moonlit forest. 383 + 384 + --- 385 + 386 + ## What We Learned 387 + 388 + 1. **Branded types create distinct types from primitives** — `DamId` and `WorkOrderId` are both strings, but the compiler treats them as incompatible. Use `brand()` to create values, and the type system prevents mixing. 389 + 390 + 2. **Typestate encodes valid states as types** — Instead of a `status: string` field, each state is its own type (`PendingWork`, `InProgressWork`, `CompletedWork`). The type itself carries the state. 391 + 392 + 3. **State transitions become functions with typed signatures** — `startWork(PendingWork) → InProgressWork` makes it impossible to start completed work or complete pending work. 393 + 394 + 4. **Literal types catch typos at compile time** — `"Pending"` is not a string, it's a specific literal type. `"Pendnig"` won't match. 395 + 396 + 5. **Invalid states become unrepresentable** — You can't construct a `CompletedWork` without going through the proper transitions. The type system enforces the business rules. 397 + 398 + --- 399 + 400 + ## Try It Yourself 401 + 402 + The complete code from this story is available as a runnable example: 403 + 404 + ```bash 405 + bun examples/stories/beavers-big-system/01-branded-typestate.ts 406 + ``` 407 + 408 + Try creating work orders with mismatched ID types, or completing work that was never started. Watch the compiler catch your mistakes. 409 + 410 + --- 411 + 412 + *Next: [The Categorization](./02-the-categorization.md) — where the animals learn to distinguish different kinds of work*
+495
docs/guides/stories/beavers-big-system/02-the-categorization.md
··· 1 + # The Categorization 2 + 3 + *Part 2 of Beaver's Big System* 4 + 5 + > In which the animals learn that different work requires different data, 6 + > and the compiler can ensure you handle every case. 7 + 8 + --- 9 + 10 + The morning sun filtered through the canopy as the animals reconvened. Beaver had brought fresh leaves, ready to continue the rebuild. 11 + 12 + "Yesterday we fixed the ID confusion and the impossible states," Owl began. "But Badger raised an important point." 13 + 14 + "Different work needs different handling," Badger repeated. "My dam repairs need water level readings. You can't schedule work when the river's too high." 15 + 16 + "And bridge inspections need weight capacity checks," added Rabbit. "I won't cross a bridge until I know it can hold me." 17 + 18 + Beaver pulled out the current type definition: 19 + 20 + ```typescript 21 + type TargetId = DamId | BridgeId | PathId | BurrowId 22 + 23 + type PendingWork = { 24 + readonly _state: "Pending" 25 + readonly id: WorkOrderId 26 + readonly targetId: TargetId 27 + readonly description: string 28 + readonly createdAt: number 29 + } 30 + ``` 31 + 32 + "The problem is `description: string`," Owl observed. "It's just... text. Someone writes 'check water level' for dam work, 'assess capacity' for bridges. But nothing *enforces* that structure." 33 + 34 + "I added a `type` field," Beaver admitted. "But look what happened." 35 + 36 + ```typescript 37 + // Beaver's first attempt 38 + type WorkType = string 39 + 40 + const work1 = { type: "Dam", ... } 41 + const work2 = { type: "dam", ... } 42 + const work3 = { type: "DAM", ... } 43 + const work4 = { type: "water-structure", ... } // Fox's contribution 44 + ``` 45 + 46 + Fox shrugged. "I thought 'dam' was too informal." 47 + 48 + --- 49 + 50 + "The real issue," said Owl, "is that different kinds of work have different *shapes*. Dam repairs need a water level. Bridge work needs a weight capacity. Path clearing needs equipment type. They're not the same structure at all." 51 + 52 + She sketched in the dirt: 53 + 54 + ```typescript 55 + // What we actually need 56 + 57 + Dam Repair: 58 + - damId: DamId 59 + - waterLevel: number 60 + - repairType: "structural" | "spillway" | "foundation" 61 + 62 + Bridge Work: 63 + - bridgeId: BridgeId 64 + - weightCapacity: number 65 + - inspectionRequired: boolean 66 + 67 + Path Clearing: 68 + - pathId: PathId 69 + - equipment: "saw" | "shovel" | "rake" 70 + - estimatedLength: number 71 + 72 + Burrow Inspection: 73 + - burrowId: BurrowId 74 + - size: "small" | "medium" | "large" 75 + - occupant: string 76 + ``` 77 + 78 + "Four different structures," said Beaver. "How do we represent that in one system?" 79 + 80 + "With a *discriminated union*," said Owl. "Also called an algebraic data type, or ADT." 81 + 82 + --- 83 + 84 + "Watch carefully." Owl wrote new types: 85 + 86 + ```typescript 87 + type DamRepair = { 88 + readonly _tag: "DamRepair" 89 + readonly damId: DamId 90 + readonly waterLevel: number 91 + readonly repairType: "structural" | "spillway" | "foundation" 92 + } 93 + 94 + type BridgeWork = { 95 + readonly _tag: "BridgeWork" 96 + readonly bridgeId: BridgeId 97 + readonly weightCapacity: number 98 + readonly inspectionRequired: boolean 99 + } 100 + 101 + type PathClearing = { 102 + readonly _tag: "PathClearing" 103 + readonly pathId: PathId 104 + readonly equipment: "saw" | "shovel" | "rake" 105 + readonly estimatedLength: number 106 + } 107 + 108 + type BurrowInspection = { 109 + readonly _tag: "BurrowInspection" 110 + readonly burrowId: BurrowId 111 + readonly size: "small" | "medium" | "large" 112 + readonly occupant: string 113 + } 114 + 115 + // The union of all work types 116 + type WorkDetails = 117 + | DamRepair 118 + | BridgeWork 119 + | PathClearing 120 + | BurrowInspection 121 + ``` 122 + 123 + "Each variant has a `_tag` field," Owl explained. "That's the *discriminant*. When you have a `WorkDetails` value, you can check the `_tag` to know which variant you're dealing with." 124 + 125 + "Like a label," said Beaver. 126 + 127 + "Exactly. But unlike your string `type` field, these are the *only* valid values. There's no `"dam"` vs `"Dam"` confusion. Either it's `"DamRepair"` or the code doesn't compile." 128 + 129 + --- 130 + 131 + "Now here's where it gets powerful." Owl pulled out a fresh leaf. "What if I need to estimate how long a job will take?" 132 + 133 + ```typescript 134 + // Bad approach — manual checking, easy to forget cases 135 + const estimateTime = (work: WorkDetails): number => { 136 + if (work._tag === "DamRepair") { 137 + return work.waterLevel > 5 ? 8 : 4 // High water = longer job 138 + } 139 + if (work._tag === "BridgeWork") { 140 + return work.inspectionRequired ? 6 : 3 141 + } 142 + // Oops, forgot PathClearing and BurrowInspection! 143 + return 2 // Default... but is this right? 144 + } 145 + ``` 146 + 147 + "This compiles," Owl said. "TypeScript doesn't complain. But when someone adds a `PathClearing` work order, it silently gets the wrong estimate." 148 + 149 + Rabbit's ears flattened. "That's the kind of bug that hides for weeks." 150 + 151 + "Now watch this." Owl rewrote the function: 152 + 153 + ```typescript 154 + import { match } from "purus-ts" 155 + 156 + const estimateTime = (work: WorkDetails): number => 157 + match(work)({ 158 + DamRepair: ({ waterLevel }) => waterLevel > 5 ? 8 : 4, 159 + BridgeWork: ({ inspectionRequired }) => inspectionRequired ? 6 : 3, 160 + PathClearing: ({ estimatedLength }) => Math.ceil(estimatedLength / 100), 161 + BurrowInspection: ({ size }) => 162 + size === "large" ? 3 : size === "medium" ? 2 : 1, 163 + }) 164 + ``` 165 + 166 + "This is *exhaustive* pattern matching. The `match` function requires a handler for *every* variant. If you forget one—" 167 + 168 + She erased the `BurrowInspection` line. 169 + 170 + ```typescript 171 + // Missing BurrowInspection handler 172 + const estimateTime = (work: WorkDetails): number => 173 + match(work)({ 174 + DamRepair: ({ waterLevel }) => waterLevel > 5 ? 8 : 4, 175 + BridgeWork: ({ inspectionRequired }) => inspectionRequired ? 6 : 3, 176 + PathClearing: ({ estimatedLength }) => Math.ceil(estimatedLength / 100), 177 + // Error: Property 'BurrowInspection' is missing in type... 178 + }) 179 + ``` 180 + 181 + "The compiler catches it immediately!" 182 + 183 + --- 184 + 185 + Fox had been studying the examples. "What happens when we add a new type of work? Like... tree trimming?" 186 + 187 + "That's the best part." Owl grinned — as much as an owl can grin. "Let's add it." 188 + 189 + ```typescript 190 + type TreeTrimming = { 191 + readonly _tag: "TreeTrimming" 192 + readonly treeId: TreeId 193 + readonly height: number 194 + readonly species: string 195 + } 196 + 197 + // Updated union 198 + type WorkDetails = 199 + | DamRepair 200 + | BridgeWork 201 + | PathClearing 202 + | BurrowInspection 203 + | TreeTrimming // New! 204 + ``` 205 + 206 + "Now every `match` in the codebase breaks." 207 + 208 + ```typescript 209 + const estimateTime = (work: WorkDetails): number => 210 + match(work)({ 211 + DamRepair: ({ waterLevel }) => waterLevel > 5 ? 8 : 4, 212 + BridgeWork: ({ inspectionRequired }) => inspectionRequired ? 6 : 3, 213 + PathClearing: ({ estimatedLength }) => Math.ceil(estimatedLength / 100), 214 + BurrowInspection: ({ size }) => 215 + size === "large" ? 3 : size === "medium" ? 2 : 1, 216 + // Error: Property 'TreeTrimming' is missing in type... 217 + }) 218 + ``` 219 + 220 + "The compiler tells you exactly where to add handling. You can't forget." 221 + 222 + "That's..." Fox paused. "That's actually brilliant." 223 + 224 + --- 225 + 226 + Beaver started rebuilding the system with the new concepts: 227 + 228 + ```typescript 229 + import { type Branded, brand, match, pipe } from "purus-ts" 230 + 231 + // ================================================================= 232 + // Branded IDs 233 + // ================================================================= 234 + 235 + type WorkOrderId = Branded<string, "WorkOrderId"> 236 + type DamId = Branded<string, "DamId"> 237 + type BridgeId = Branded<string, "BridgeId"> 238 + type PathId = Branded<string, "PathId"> 239 + type BurrowId = Branded<string, "BurrowId"> 240 + 241 + // ================================================================= 242 + // Work Detail Types — Each kind of work has its own shape 243 + // ================================================================= 244 + 245 + type DamRepair = { 246 + readonly _tag: "DamRepair" 247 + readonly damId: DamId 248 + readonly waterLevel: number 249 + readonly repairType: "structural" | "spillway" | "foundation" 250 + } 251 + 252 + type BridgeWork = { 253 + readonly _tag: "BridgeWork" 254 + readonly bridgeId: BridgeId 255 + readonly weightCapacity: number 256 + readonly inspectionRequired: boolean 257 + } 258 + 259 + type PathClearing = { 260 + readonly _tag: "PathClearing" 261 + readonly pathId: PathId 262 + readonly equipment: "saw" | "shovel" | "rake" 263 + readonly estimatedLength: number 264 + } 265 + 266 + type BurrowInspection = { 267 + readonly _tag: "BurrowInspection" 268 + readonly burrowId: BurrowId 269 + readonly size: "small" | "medium" | "large" 270 + readonly occupant: string 271 + } 272 + 273 + type WorkDetails = 274 + | DamRepair 275 + | BridgeWork 276 + | PathClearing 277 + | BurrowInspection 278 + 279 + // ================================================================= 280 + // Work Order with Details 281 + // ================================================================= 282 + 283 + type WorkOrder<S extends string, D extends WorkDetails> = { 284 + readonly _state: S 285 + readonly id: WorkOrderId 286 + readonly details: D 287 + readonly createdAt: number 288 + } 289 + 290 + type PendingWork<D extends WorkDetails> = WorkOrder<"Pending", D> 291 + type InProgressWork<D extends WorkDetails> = WorkOrder<"InProgress", D> & { 292 + readonly startedAt: number 293 + readonly assignee: string 294 + } 295 + type CompletedWork<D extends WorkDetails> = WorkOrder<"Completed", D> & { 296 + readonly startedAt: number 297 + readonly completedAt: number 298 + readonly assignee: string 299 + } 300 + ``` 301 + 302 + "I combined the typestate from yesterday with the discriminated union," Beaver explained. "Each work order has *both* a state and a details type." 303 + 304 + --- 305 + 306 + "Let's write some utility functions," Owl suggested. 307 + 308 + ```typescript 309 + // Estimate time based on work type 310 + const estimateHours = (details: WorkDetails): number => 311 + match(details)({ 312 + DamRepair: ({ waterLevel, repairType }) => 313 + repairType === "foundation" ? 12 : 314 + waterLevel > 5 ? 8 : 4, 315 + BridgeWork: ({ inspectionRequired, weightCapacity }) => 316 + inspectionRequired ? 6 : 317 + weightCapacity < 1000 ? 2 : 4, 318 + PathClearing: ({ estimatedLength, equipment }) => 319 + equipment === "saw" ? Math.ceil(estimatedLength / 50) : 320 + Math.ceil(estimatedLength / 100), 321 + BurrowInspection: ({ size }) => 322 + size === "large" ? 3 : size === "medium" ? 2 : 1, 323 + }) 324 + 325 + // Get a human-readable description 326 + const describeWork = (details: WorkDetails): string => 327 + match(details)({ 328 + DamRepair: ({ repairType }) => 329 + `Dam ${repairType} repair`, 330 + BridgeWork: ({ inspectionRequired }) => 331 + inspectionRequired ? "Bridge inspection and repair" : "Bridge maintenance", 332 + PathClearing: ({ equipment, estimatedLength }) => 333 + `Clear ${estimatedLength}m of path using ${equipment}`, 334 + BurrowInspection: ({ occupant, size }) => 335 + `Inspect ${size} burrow (${occupant}'s home)`, 336 + }) 337 + 338 + // Check if work requires special equipment 339 + const requiresEquipment = (details: WorkDetails): boolean => 340 + match(details)({ 341 + DamRepair: () => true, // Always need tools 342 + BridgeWork: () => true, 343 + PathClearing: () => true, 344 + BurrowInspection: () => false, // Just looking 345 + }) 346 + ``` 347 + 348 + "Every function that operates on `WorkDetails` is forced to consider all cases," Owl noted. "You can't accidentally treat bridge work like dam repair." 349 + 350 + --- 351 + 352 + Rabbit had been thinking. "What if I only want to handle some cases? Like, I only care about burrow inspections." 353 + 354 + Owl nodded. "There's a variant for that — `matchOr`. You provide handlers for the cases you care about, and a default for the rest." 355 + 356 + ```typescript 357 + import { matchOr } from "purus-ts" 358 + 359 + // Only Rabbit cares about burrows 360 + const isRabbitRelevant = (details: WorkDetails): boolean => 361 + matchOr(false)(details)({ 362 + BurrowInspection: () => true, 363 + }) 364 + ``` 365 + 366 + "But use it carefully," Owl warned. "If you add a new work type that Rabbit *should* care about, the default will swallow it silently. Exhaustive matching is safer." 367 + 368 + "When in doubt, handle every case," said Fox. He'd been won over. 369 + 370 + --- 371 + 372 + Beaver demonstrated the complete system: 373 + 374 + ```typescript 375 + // Create some work orders 376 + const damWork: PendingWork<DamRepair> = { 377 + _state: "Pending", 378 + id: brand("wo-1"), 379 + details: { 380 + _tag: "DamRepair", 381 + damId: brand("dam-north"), 382 + waterLevel: 3.2, 383 + repairType: "spillway", 384 + }, 385 + createdAt: Date.now(), 386 + } 387 + 388 + const burrowWork: PendingWork<BurrowInspection> = { 389 + _state: "Pending", 390 + id: brand("wo-2"), 391 + details: { 392 + _tag: "BurrowInspection", 393 + burrowId: brand("burrow-rabbit-1"), 394 + size: "medium", 395 + occupant: "Rabbit", 396 + }, 397 + createdAt: Date.now(), 398 + } 399 + 400 + // Process work orders 401 + console.log(describeWork(damWork.details)) 402 + // "Dam spillway repair" 403 + 404 + console.log(describeWork(burrowWork.details)) 405 + // "Inspect medium burrow (Rabbit's home)" 406 + 407 + console.log(`Dam work estimate: ${estimateHours(damWork.details)} hours`) 408 + // "Dam work estimate: 4 hours" 409 + 410 + console.log(`Burrow work estimate: ${estimateHours(burrowWork.details)} hours`) 411 + // "Burrow work estimate: 2 hours" 412 + ``` 413 + 414 + "Each work order knows exactly what kind of work it is," Beaver said proudly. "And the compiler knows too." 415 + 416 + --- 417 + 418 + "Add bridge work to the mix," Fox suggested. 419 + 420 + ```typescript 421 + const bridgeWork: PendingWork<BridgeWork> = { 422 + _state: "Pending", 423 + id: brand("wo-3"), 424 + details: { 425 + _tag: "BridgeWork", 426 + bridgeId: brand("bridge-east"), 427 + weightCapacity: 500, 428 + inspectionRequired: true, 429 + }, 430 + createdAt: Date.now(), 431 + } 432 + 433 + // Works seamlessly 434 + console.log(describeWork(bridgeWork.details)) 435 + // "Bridge inspection and repair" 436 + 437 + console.log(`Estimate: ${estimateHours(bridgeWork.details)} hours`) 438 + // "Estimate: 6 hours" 439 + ``` 440 + 441 + "And if we add tree trimming next month—" Fox started. 442 + 443 + "Every function with `match` will immediately show where we need to add handling," Owl finished. "The compiler guides you to completeness." 444 + 445 + --- 446 + 447 + The afternoon sun was warm. The animals looked at their new system with satisfaction. 448 + 449 + "We have distinct ID types," Beaver summarized. "Typestate for work progression. And now discriminated unions for different kinds of work." 450 + 451 + "Each piece of work carries exactly the data it needs," said Owl. "No more guessing what fields apply to what type." 452 + 453 + Badger cleared his throat. "This is good. But I have another concern." 454 + 455 + The animals turned to him. 456 + 457 + "We have dozens of work orders now. They need to be prioritized. Someone built a queue, but..." He shook his head. "Last night it crashed. Called `getNext()` on an empty queue." 458 + 459 + "And the 'sorted by priority' list," Rabbit added, "wasn't actually sorted. Someone appended a new item and forgot to re-sort." 460 + 461 + "Can the type system help with *that*?" asked Fox, genuinely curious now. 462 + 463 + Owl gathered her leaves. "It can. Tomorrow, we'll talk about *tracked arrays*. Properties like 'non-empty' and 'sorted' — encoded in the type itself." 464 + 465 + Beaver was already sketching notes for tomorrow. The system was taking shape, one type at a time. 466 + 467 + --- 468 + 469 + ## What We Learned 470 + 471 + 1. **Discriminated unions group related variants** — `WorkDetails` is one of four specific types, each with its own structure. The `_tag` field identifies which variant you have. 472 + 473 + 2. **Exhaustive matching with `match()`** — The `match` function requires handlers for every variant. Forget one, and the compiler tells you. 474 + 475 + 3. **Adding a variant breaks all incomplete matches** — When you add `TreeTrimming` to the union, every `match` that doesn't handle it becomes a compile error. You can't forget to update handlers. 476 + 477 + 4. **Each variant has the right data** — `DamRepair` has `waterLevel`, `BridgeWork` has `weightCapacity`. No optional fields, no runtime checks for "does this field exist?" 478 + 479 + 5. **Use `matchOr` for partial matching** — When you only care about some variants, `matchOr(defaultValue)(...)` handles the rest. But prefer exhaustive matching when possible. 480 + 481 + --- 482 + 483 + ## Try It Yourself 484 + 485 + The complete code from this story is available as a runnable example: 486 + 487 + ```bash 488 + bun examples/stories/beavers-big-system/02-adt-matching.ts 489 + ``` 490 + 491 + Try adding a new work type like `TreeTrimming` or `NestRepair`. Watch the compiler guide you to every place that needs updating. 492 + 493 + --- 494 + 495 + *Next: [The Queue](./03-the-queue.md) — where the animals learn that arrays can track their own properties*
+468
docs/guides/stories/beavers-big-system/03-the-queue.md
··· 1 + # The Queue 2 + 3 + *Part 3 of Beaver's Big System* 4 + 5 + > In which the animals learn that arrays can remember their properties, 6 + > and valid values should prove themselves. 7 + 8 + --- 9 + 10 + It was past midnight when the crash happened. 11 + 12 + Beaver had built the priority queue three days ago — a simple system to order work by urgency. High-priority items at the front, low-priority at the back. Every morning, the crew would check the queue and grab the next job. 13 + 14 + But tonight, the night crew found an empty queue and called `getNext()` anyway. 15 + 16 + The system exploded. 17 + 18 + "Array index out of bounds," Fox read from the error log. "At line 47: `return queue[0]`." 19 + 20 + "But there should always be work in the queue," Beaver protested. "I checked before leaving." 21 + 22 + "Apparently not." Badger rubbed his eyes. "The bridge crew finished early and cleared everything. Then the dam crew came in, saw the queue, and..." 23 + 24 + "Boom," said Fox. 25 + 26 + --- 27 + 28 + Morning came too soon. The animals gathered, bleary-eyed, around Owl's teaching stump. 29 + 30 + "The queue had three problems," Owl began, drawing in the dirt. "First, the crash we saw last night — accessing an empty array." 31 + 32 + ```typescript 33 + // Beaver's original queue 34 + const getNext = (queue: WorkOrder[]): WorkOrder => 35 + queue[0] // Crashes if queue is empty! 36 + ``` 37 + 38 + "Second," Owl continued, "the queue was supposed to be sorted by priority. But look at this." 39 + 40 + ```typescript 41 + const addWork = (queue: WorkOrder[], work: WorkOrder): WorkOrder[] => 42 + [...queue, work] // Just appends — doesn't maintain sort order! 43 + ``` 44 + 45 + "Someone adds a high-priority item, and it goes to the *end* of the queue. The sorted property is gone." 46 + 47 + Rabbit raised a paw. "And the third problem?" 48 + 49 + Owl pointed to a leaf-card. "Priority values. This one has priority `-1`. Someone thought negative meant 'urgent'. This one has priority `999999`. Someone thought bigger was better." 50 + 51 + "So priorities are just... whatever anyone types?" Fox asked. 52 + 53 + "Exactly. No validation. No constraints." 54 + 55 + --- 56 + 57 + "Let's fix the priority first," said Owl. "It's the foundation everything else builds on." 58 + 59 + She wrote: 60 + 61 + ```typescript 62 + import { type Branded, brand, type Option, some, none } from "purus-ts" 63 + 64 + // A priority is a non-negative integer 65 + type Priority = Branded<number, "Priority"> 66 + 67 + // Creating a valid priority — returns Option because it might fail 68 + const priority = (n: number): Option<Priority> => 69 + Number.isInteger(n) && n >= 0 70 + ? some(brand(n)) 71 + : none 72 + ``` 73 + 74 + "This is a *refinement*," Owl explained. "A `Priority` isn't just any number — it's a number that's passed validation. Non-negative, integer, branded." 75 + 76 + "But it returns an `Option`," Beaver noted. 77 + 78 + "Because validation can fail. If someone passes `-1`, they get `none`. The type system forces them to handle that case." 79 + 80 + ```typescript 81 + import { match } from "purus-ts" 82 + 83 + const result = priority(-1) 84 + 85 + match(result)({ 86 + Some: ({ value }) => console.log(`Valid priority: ${value}`), 87 + None: () => console.log("Invalid priority — must be non-negative integer"), 88 + }) 89 + // Output: "Invalid priority — must be non-negative integer" 90 + ``` 91 + 92 + "No more mystery `-1` priorities," said Rabbit, relieved. 93 + 94 + --- 95 + 96 + "Now for the array problems." Owl cleared a fresh space. "We need arrays that *remember* their properties." 97 + 98 + ```typescript 99 + import { type Arr, type NonEmpty, type Sorted, arr, nonEmpty, sortBy } from "purus-ts" 100 + 101 + // A regular array — we know nothing about it 102 + type WorkQueue = Arr<WorkOrder> 103 + 104 + // A non-empty array — guaranteed to have at least one element 105 + type NonEmptyQueue = Arr<WorkOrder, NonEmpty> 106 + 107 + // A sorted array — guaranteed to be in order 108 + type SortedQueue = Arr<WorkOrder, Sorted> 109 + 110 + // Both properties at once! 111 + type ReadyQueue = Arr<WorkOrder, NonEmpty | Sorted> 112 + ``` 113 + 114 + "These are called *phantom types*," Owl said. "The `NonEmpty` and `Sorted` markers don't exist at runtime — they're purely compile-time information. But the type system tracks them." 115 + 116 + Fox squinted. "So an `Arr<WorkOrder, NonEmpty>` is just a regular array underneath?" 117 + 118 + "Yes. But the compiler treats it differently. Functions can *require* certain properties." 119 + 120 + --- 121 + 122 + "Watch how this prevents last night's crash." Owl wrote the safe version: 123 + 124 + ```typescript 125 + import { head } from "purus-ts" 126 + 127 + // head() REQUIRES a NonEmpty array — won't compile otherwise 128 + const getNext = (queue: Arr<WorkOrder, NonEmpty>): WorkOrder => 129 + head(queue) // Safe! We KNOW there's at least one element 130 + ``` 131 + 132 + "If you try to pass a regular array—" 133 + 134 + ```typescript 135 + const emptyQueue: Arr<WorkOrder> = arr([]) 136 + 137 + // This FAILS to compile! 138 + // const work = getNext(emptyQueue) 139 + // Error: Argument of type 'Arr<WorkOrder>' is not assignable 140 + // to parameter of type 'Arr<WorkOrder, NonEmpty>' 141 + ``` 142 + 143 + "The compiler stops you *before* the crash happens." 144 + 145 + Beaver's eyes went wide. "So we can't call `getNext` on an empty queue at all?" 146 + 147 + "Not unless you prove it's non-empty first." 148 + 149 + --- 150 + 151 + "How do you prove it?" asked Rabbit. 152 + 153 + Owl wrote: 154 + 155 + ```typescript 156 + const queue: Arr<WorkOrder> = arr([...]) // Regular array, unknown if empty 157 + 158 + const result = nonEmpty(queue) // Returns Option<Arr<WorkOrder, NonEmpty>> 159 + 160 + match(result)({ 161 + Some: ({ value }) => { 162 + // Inside here, 'value' is Arr<WorkOrder, NonEmpty> 163 + const next = getNext(value) // Safe! 164 + console.log(`Next work: ${next.id}`) 165 + }, 166 + None: () => { 167 + console.log("No work in queue") 168 + }, 169 + }) 170 + ``` 171 + 172 + "The `nonEmpty` function checks at runtime and returns an `Option`. If the array is non-empty, you get `Some` with the array *rebranded* as `NonEmpty`. If it's empty, you get `None`." 173 + 174 + "So you check once, and then the type remembers," Fox said slowly. 175 + 176 + "Exactly. Check at the boundary, trust the types inside." 177 + 178 + --- 179 + 180 + "What about sorting?" Beaver asked. "The queue kept getting unsorted." 181 + 182 + Owl nodded. "Sorting works the same way. The `sortBy` function takes any array and returns one marked `Sorted`." 183 + 184 + ```typescript 185 + // Sort by priority (ascending — lower number = higher priority) 186 + const sortByPriority = sortBy<WorkOrder, never>( 187 + (work) => work.priority 188 + ) 189 + 190 + const unsorted: Arr<WorkOrder> = arr([ 191 + { id: "wo-3", priority: brand(5) }, 192 + { id: "wo-1", priority: brand(1) }, 193 + { id: "wo-2", priority: brand(3) }, 194 + ]) 195 + 196 + const sorted: Arr<WorkOrder, Sorted> = sortByPriority(unsorted) 197 + // Now the type KNOWS it's sorted 198 + ``` 199 + 200 + "But here's the key." Owl wrote more: 201 + 202 + ```typescript 203 + import { map, filter } from "purus-ts" 204 + 205 + // Mapping preserves NonEmpty but REMOVES Sorted 206 + // (because the mapped values might have different sort keys) 207 + const descriptions = pipe( 208 + sorted, 209 + map((w) => w.description) 210 + ) 211 + // Type: Arr<string, never> — Sorted property is gone! 212 + 213 + // Filtering REMOVES NonEmpty 214 + // (because filter might remove all elements) 215 + const urgent = pipe( 216 + sorted, 217 + filter((w) => w.priority < brand(3)) 218 + ) 219 + // Type: Arr<WorkOrder, never> — NonEmpty is gone! 220 + ``` 221 + 222 + "The type system tracks which operations preserve which properties. You can't accidentally assume a filtered array is still non-empty." 223 + 224 + --- 225 + 226 + "Let me rebuild the queue properly," said Beaver. 227 + 228 + ```typescript 229 + import { 230 + type Arr, 231 + type NonEmpty, 232 + type Sorted, 233 + type Branded, 234 + type Option, 235 + arr, 236 + brand, 237 + nonEmpty, 238 + sortBy, 239 + head, 240 + match, 241 + pipe, 242 + some, 243 + none, 244 + } from "purus-ts" 245 + 246 + // ================================================================= 247 + // Priority — Validated non-negative integer 248 + // ================================================================= 249 + 250 + type Priority = Branded<number, "Priority"> 251 + 252 + const priority = (n: number): Option<Priority> => 253 + Number.isInteger(n) && n >= 0 254 + ? some(brand(n)) 255 + : none 256 + 257 + // ================================================================= 258 + // Work Order (simplified for this example) 259 + // ================================================================= 260 + 261 + type WorkOrderId = Branded<string, "WorkOrderId"> 262 + 263 + type WorkOrder = { 264 + readonly id: WorkOrderId 265 + readonly description: string 266 + readonly priority: Priority 267 + } 268 + 269 + const workOrder = ( 270 + id: string, 271 + description: string, 272 + p: Priority, 273 + ): WorkOrder => ({ 274 + id: brand(id), 275 + description, 276 + priority: p, 277 + }) 278 + 279 + // ================================================================= 280 + // Queue Operations 281 + // ================================================================= 282 + 283 + type WorkQueue = Arr<WorkOrder> 284 + type ReadyQueue = Arr<WorkOrder, NonEmpty | Sorted> 285 + 286 + // Sort by priority (lower = more urgent) 287 + const sortByPriority = sortBy<WorkOrder, never>( 288 + (work) => work.priority as number, 289 + ) 290 + 291 + // Add work and re-sort to maintain ordering 292 + const addToQueue = ( 293 + queue: Arr<WorkOrder, Sorted>, 294 + work: WorkOrder, 295 + ): Arr<WorkOrder, NonEmpty | Sorted> => 296 + sortByPriority(arr([...queue, work])) as Arr<WorkOrder, NonEmpty | Sorted> 297 + 298 + // Get next work — REQUIRES non-empty queue 299 + const getNext = (queue: Arr<WorkOrder, NonEmpty>): WorkOrder => 300 + head(queue) 301 + 302 + // Try to get next work — handles empty case safely 303 + const tryGetNext = (queue: WorkQueue): Option<WorkOrder> => 304 + pipe( 305 + nonEmpty(queue), 306 + (opt) => match(opt)({ 307 + Some: ({ value }) => some(head(value)), 308 + None: () => none, 309 + }), 310 + ) 311 + ``` 312 + 313 + --- 314 + 315 + "Let's trace through the whole flow," said Owl. 316 + 317 + ```typescript 318 + // Start with an empty queue 319 + const emptyQueue: Arr<WorkOrder, Sorted> = arr([]) as Arr<WorkOrder, Sorted> 320 + 321 + // Create valid priorities 322 + const p1 = priority(1) // Option<Priority> 323 + const p5 = priority(5) 324 + const p3 = priority(3) 325 + const pBad = priority(-1) // This will be None! 326 + 327 + // Build work orders (only with valid priorities) 328 + const processValidPriority = ( 329 + id: string, 330 + desc: string, 331 + maybePriority: Option<Priority>, 332 + ): Option<WorkOrder> => 333 + match(maybePriority)({ 334 + Some: ({ value }) => some(workOrder(id, desc, value)), 335 + None: () => none, 336 + }) 337 + 338 + // Add work to queue 339 + match(processValidPriority("wo-1", "Fix spillway", p1))({ 340 + Some: ({ value: work }) => { 341 + const queue1 = addToQueue(emptyQueue, work) 342 + // queue1 is now NonEmpty | Sorted 343 + 344 + match(processValidPriority("wo-2", "Inspect bridge", p5))({ 345 + Some: ({ value: work2 }) => { 346 + const queue2 = addToQueue(queue1, work2) 347 + 348 + match(processValidPriority("wo-3", "Clear path", p3))({ 349 + Some: ({ value: work3 }) => { 350 + const queue3 = addToQueue(queue2, work3) 351 + 352 + // Queue is guaranteed NonEmpty and Sorted 353 + const next = getNext(queue3) 354 + console.log(`Next job: ${next.description}`) 355 + // Output: "Next job: Fix spillway" (priority 1) 356 + }, 357 + None: () => console.log("Invalid priority for wo-3"), 358 + }) 359 + }, 360 + None: () => console.log("Invalid priority for wo-2"), 361 + }) 362 + }, 363 + None: () => console.log("Invalid priority for wo-1"), 364 + }) 365 + 366 + // Invalid priority is caught 367 + match(processValidPriority("wo-bad", "Urgent!!!", pBad))({ 368 + Some: () => console.log("This won't print"), 369 + None: () => console.log("Rejected: -1 is not a valid priority"), 370 + }) 371 + // Output: "Rejected: -1 is not a valid priority" 372 + ``` 373 + 374 + --- 375 + 376 + Fox leaned back. "So let me make sure I understand. The `Priority` type guarantees the value is valid. The `NonEmpty` property guarantees we can call `head`. The `Sorted` property guarantees the order." 377 + 378 + "And the type system tracks all of it," Owl confirmed. "You don't need runtime checks scattered everywhere — you check once at the boundary, and the types carry the proof." 379 + 380 + "Last night's crash..." Beaver said slowly. 381 + 382 + "Would have been a compile error, not a runtime crash. The code wouldn't even build unless you handled the empty case." 383 + 384 + --- 385 + 386 + Badger had been quiet, watching. Now he smiled. 387 + 388 + "You know," he said, "I remember when forest maintenance was simpler. Leaves in a box. Rocks for priority. Shouting to coordinate." 389 + 390 + The animals chuckled. 391 + 392 + "But we were also smaller then," Badger continued. "Fewer dams, fewer bridges, fewer animals. The old ways worked when you could hold everything in your head." 393 + 394 + He looked at the leaves covered in types and functions. 395 + 396 + "This forest elected a president using validated ballots, counted with proper error handling, announced with reliable effects. Now it tracks maintenance with branded IDs, typestate transitions, discriminated work types, and tracked arrays." 397 + 398 + "Is that good?" Rabbit asked. 399 + 400 + Badger nodded slowly. "It's necessary. When systems grow, you can't rely on everyone remembering the rules. You need the rules *encoded*. So the system itself prevents mistakes." 401 + 402 + He stood to leave. 403 + 404 + "This is the same forest that once counted votes with leaves in a box," Badger said. "Look how far we've come." 405 + 406 + --- 407 + 408 + The sun was setting. The animals gathered their notes, their leaves, their diagrams. 409 + 410 + Beaver looked at the queue implementation — sorted, non-empty, priority-validated. No more midnight crashes. No more negative priorities. No more unsorted surprises. 411 + 412 + "Tomorrow," Beaver said, "we put it all together. Branded IDs. Typestate. ADTs. Tracked arrays. The real system." 413 + 414 + Fox stretched. "I'll admit, I was skeptical. 'Too many types,' I kept saying." 415 + 416 + "And now?" asked Owl. 417 + 418 + Fox paused. "Now I think... it's not about more types. It's about *better* types. Types that mean something. Types that prevent bugs instead of just describing data." 419 + 420 + Rabbit nodded, her worries finally settled. Every anxiety she'd voiced — mixed-up IDs, impossible states, missing cases, empty queues — had become a compile-time check. 421 + 422 + "Thank you, Owl," she said quietly. 423 + 424 + Owl ruffled her feathers. "Thank yourselves. You built it. I just showed you the patterns." 425 + 426 + The animals dispersed into the twilight forest. Behind them, the Work Order Tracker hummed quietly, processing jobs with types that couldn't lie. 427 + 428 + --- 429 + 430 + ## What We Learned 431 + 432 + 1. **Refinements validate values at creation** — A `Priority` isn't just a number; it's a number that passed validation. The `Option` return type forces callers to handle invalid input. 433 + 434 + 2. **Phantom types track array properties** — `Arr<T, NonEmpty>` and `Arr<T, Sorted>` carry compile-time information about the array's state. The markers don't exist at runtime. 435 + 436 + 3. **Functions can require properties** — `head(queue: Arr<T, NonEmpty>)` won't accept a regular array. The type system enforces the precondition. 437 + 438 + 4. **Operations affect properties correctly** — `map` preserves `NonEmpty` but removes `Sorted`. `filter` removes `NonEmpty`. The types track what's preserved. 439 + 440 + 5. **Check at boundaries, trust inside** — Use `nonEmpty()` once to prove an array isn't empty, then work with the `NonEmpty` type freely. No repeated runtime checks. 441 + 442 + --- 443 + 444 + ## Try It Yourself 445 + 446 + The complete code from this story is available as a runnable example: 447 + 448 + ```bash 449 + bun examples/stories/beavers-big-system/03-tracked-arrays.ts 450 + ``` 451 + 452 + Try creating priorities with invalid values, or calling `head` on an array that hasn't been checked with `nonEmpty`. Watch the compiler guide you to safety. 453 + 454 + --- 455 + 456 + ## The Complete Trilogy 457 + 458 + | Part | Story | Concepts | 459 + |------|-------|----------| 460 + | 1 | [The Mixup](./01-the-mixup.md) | Branded Types + Typestate | 461 + | 2 | [The Categorization](./02-the-categorization.md) | ADTs + Pattern Matching | 462 + | 3 | The Queue (this story) | Tracked Arrays + Refinements | 463 + 464 + Together, these patterns form a toolkit for *making invalid states unrepresentable*. The type system becomes your first line of defense against bugs — catching errors at compile time instead of runtime. 465 + 466 + --- 467 + 468 + *Return to [Beaver's Big System overview](./) or explore [The Forest Election](../forest-election/) trilogy*
+95
docs/guides/stories/beavers-big-system/README.md
··· 1 + # Beaver's Big System 2 + 3 + A story in three parts about building maintainable maintenance software. 4 + 5 + --- 6 + 7 + ## The Story 8 + 9 + After the successful forest election, the animals turn to infrastructure. Beaver, eager to help, builds "The System" — a grand work order tracker. But Beaver used strings everywhere: `"dam-1"`, `"started"`, `"in_progress"`. Soon: 10 + - Work orders for dams are accidentally applied to bridges 11 + - Things marked "done" were never "started" 12 + - Nobody knows which status values are even valid 13 + 14 + Owl helps rebuild it properly, teaching patterns that prevent these bugs at compile time. 15 + 16 + --- 17 + 18 + ## Characters 19 + 20 + | Character | Personality | Role in the Code | 21 + |-----------|-------------|------------------| 22 + | **Owl** | Methodical, patient | Guides the refactoring with types | 23 + | **Beaver** | Eager, practical | Built "The System" with good intentions, now sees it crumbling | 24 + | **Fox** | Skeptical, pragmatic | "I told you this was over-engineered" (but becomes a convert) | 25 + | **Rabbit** | Anxious, detail-oriented | Her worries become the error cases | 26 + | **Badger** | Wise, experienced | Remembers when forest maintenance was simpler | 27 + 28 + --- 29 + 30 + ## Parts 31 + 32 + ### Part 1: [The Mixup](./01-the-mixup.md) 33 + 34 + Beaver's system uses plain strings for IDs and status. Fox accidentally marks `bridge-east` work as done using `dam-north`'s ID. Rabbit finds a work order with status `"in_progerss"` (typo). Badger discovers work marked `"done"` that was never `"started"`. 35 + 36 + **Concepts:** Branded Types + Typestate 37 + 38 + **You'll learn:** 39 + - Why plain strings let bugs slip through 40 + - Creating distinct ID types with `Branded<T, B>` 41 + - Encoding valid state transitions with typestate 42 + - Making impossible states unrepresentable 43 + 44 + --- 45 + 46 + ### Part 2: [The Categorization](./02-the-categorization.md) 47 + 48 + All work orders are treated identically, but different infrastructure needs different handling. Dam repairs need water level checks. Bridge work needs weight capacity assessments. Beaver's `type: string` field leads to `"Dam"`, `"dam"`, and `"water-structure"` chaos. 49 + 50 + **Concepts:** ADTs (Discriminated Unions) + Pattern Matching 51 + 52 + **You'll learn:** 53 + - Modeling variants with discriminated unions 54 + - Exhaustive pattern matching with `match()` 55 + - Compiler-enforced completeness when adding new types 56 + - Using `_tag` for type-safe dispatch 57 + 58 + --- 59 + 60 + ### Part 3: [The Queue](./03-the-queue.md) 61 + 62 + Work orders need prioritization. Someone calls `getNext()` on an empty queue — runtime crash. The "sorted by priority" list isn't actually sorted after appending. Negative priority values sneak in. 63 + 64 + **Concepts:** Tracked Arrays + Refinements 65 + 66 + **You'll learn:** 67 + - Phantom types that track array properties 68 + - `NonEmpty` arrays that can't crash on `head()` 69 + - `Sorted` arrays with guaranteed ordering 70 + - Refinement types for validated values 71 + 72 + --- 73 + 74 + ## Running the Code 75 + 76 + Each part has a matching example file: 77 + 78 + ```bash 79 + # Part 1: Branded Types + Typestate 80 + bun examples/stories/beavers-big-system/01-branded-typestate.ts 81 + 82 + # Part 2: ADTs + Pattern Matching 83 + bun examples/stories/beavers-big-system/02-adt-matching.ts 84 + 85 + # Part 3: Tracked Arrays + Refinements 86 + bun examples/stories/beavers-big-system/03-tracked-arrays.ts 87 + ``` 88 + 89 + --- 90 + 91 + ## See Also 92 + 93 + - [Stories overview](../) — Other available stories 94 + - [The Forest Election](../forest-election/) — The prequel trilogy 95 + - [Branded Types and Refinements](../../concepts/01-branded-types-and-refinements.md) — Technical deep-dive
+260
examples/stories/beavers-big-system/01-branded-typestate.ts
··· 1 + /** 2 + * Beaver's Big System — Part 1: Branded Types + Typestate 3 + * ======================================================== 4 + * 5 + * This example accompanies the story at: 6 + * docs/guides/stories/beavers-big-system/01-the-mixup.md 7 + * 8 + * It demonstrates how branded types prevent ID mixups and how 9 + * typestate ensures valid state transitions at compile time. 10 + * 11 + * Run with: bun examples/stories/beavers-big-system/01-branded-typestate.ts 12 + */ 13 + 14 + import { type Branded, brand } from "../../../src/index" 15 + 16 + // ============================================================================= 17 + // Branded IDs — Each infrastructure type has its own distinct ID type 18 + // ============================================================================= 19 + 20 + type WorkOrderId = Branded<string, "WorkOrderId"> 21 + type DamId = Branded<string, "DamId"> 22 + type BridgeId = Branded<string, "BridgeId"> 23 + type PathId = Branded<string, "PathId"> 24 + type BurrowId = Branded<string, "BurrowId"> 25 + 26 + // Helper functions to create branded IDs 27 + const workOrderId = (id: string): WorkOrderId => brand(id) 28 + const damId = (id: string): DamId => brand(id) 29 + const bridgeId = (id: string): BridgeId => brand(id) 30 + const pathId = (id: string): PathId => brand(id) 31 + const burrowId = (id: string): BurrowId => brand(id) 32 + 33 + // ============================================================================= 34 + // Typestate — Work orders progress through defined states 35 + // ============================================================================= 36 + 37 + // Union of all possible target IDs 38 + type TargetId = DamId | BridgeId | PathId | BurrowId 39 + 40 + // Each state is a distinct type with appropriate fields 41 + type PendingWork = { 42 + readonly _state: "Pending" 43 + readonly id: WorkOrderId 44 + readonly targetId: TargetId 45 + readonly description: string 46 + readonly createdAt: number 47 + } 48 + 49 + type InProgressWork = { 50 + readonly _state: "InProgress" 51 + readonly id: WorkOrderId 52 + readonly targetId: TargetId 53 + readonly description: string 54 + readonly createdAt: number 55 + readonly startedAt: number 56 + readonly assignee: string 57 + } 58 + 59 + type CompletedWork = { 60 + readonly _state: "Completed" 61 + readonly id: WorkOrderId 62 + readonly targetId: TargetId 63 + readonly description: string 64 + readonly createdAt: number 65 + readonly startedAt: number 66 + readonly completedAt: number 67 + readonly assignee: string 68 + readonly notes: string 69 + } 70 + 71 + // The union of all possible work order states 72 + type WorkOrder = PendingWork | InProgressWork | CompletedWork 73 + 74 + // ============================================================================= 75 + // State Transitions — Functions enforce valid progressions 76 + // ============================================================================= 77 + 78 + /** 79 + * Create a new pending work order. 80 + */ 81 + const createWorkOrder = ( 82 + id: WorkOrderId, 83 + targetId: TargetId, 84 + description: string, 85 + ): PendingWork => ({ 86 + _state: "Pending", 87 + id, 88 + targetId, 89 + description, 90 + createdAt: Date.now(), 91 + }) 92 + 93 + /** 94 + * Start work on a pending order. 95 + * Only accepts PendingWork — cannot start work that's already started! 96 + */ 97 + const startWork = (work: PendingWork, assignee: string): InProgressWork => ({ 98 + ...work, 99 + _state: "InProgress", 100 + startedAt: Date.now(), 101 + assignee, 102 + }) 103 + 104 + /** 105 + * Complete work that's in progress. 106 + * Only accepts InProgressWork — cannot complete work that wasn't started! 107 + */ 108 + const completeWork = (work: InProgressWork, notes: string): CompletedWork => ({ 109 + ...work, 110 + _state: "Completed", 111 + completedAt: Date.now(), 112 + notes, 113 + }) 114 + 115 + // ============================================================================= 116 + // Display Helpers 117 + // ============================================================================= 118 + 119 + // Using switch for exhaustive matching on _state discriminant 120 + // TypeScript ensures all cases are handled 121 + const describeWorkOrder = (work: WorkOrder): string => { 122 + switch (work._state) { 123 + case "Pending": 124 + return `[PENDING] ${work.id}: ${work.description}` 125 + case "InProgress": 126 + return `[IN PROGRESS] ${work.id}: ${work.description} (assigned to ${work.assignee})` 127 + case "Completed": 128 + return `[COMPLETED] ${work.id}: ${work.description} - ${work.notes}` 129 + } 130 + } 131 + 132 + // ============================================================================= 133 + // Demo 134 + // ============================================================================= 135 + 136 + console.log("=== Beaver's Big System: Branded Types + Typestate ===\n") 137 + 138 + // Create branded IDs — each type is distinct 139 + const wo1 = workOrderId("wo-1") 140 + const wo2 = workOrderId("wo-2") 141 + const damNorth = damId("dam-north") 142 + const bridgeEast = bridgeId("bridge-east") 143 + 144 + console.log("Creating work orders with proper branded IDs...\n") 145 + 146 + // Create pending work orders 147 + const damRepair = createWorkOrder(wo1, damNorth, "Repair spillway crack") 148 + const bridgeInspection = createWorkOrder(wo2, bridgeEast, "Inspect support beams") 149 + 150 + console.log(describeWorkOrder(damRepair)) 151 + console.log(describeWorkOrder(bridgeInspection)) 152 + 153 + // Progress the dam repair through states 154 + console.log("\nStarting dam repair work...") 155 + const damInProgress = startWork(damRepair, "Beaver") 156 + console.log(describeWorkOrder(damInProgress)) 157 + 158 + console.log("\nCompleting dam repair...") 159 + const damCompleted = completeWork(damInProgress, "Sealed with river clay") 160 + console.log(describeWorkOrder(damCompleted)) 161 + 162 + // ============================================================================= 163 + // Type Safety Demonstrations 164 + // ============================================================================= 165 + 166 + console.log("\n" + "=".repeat(60)) 167 + console.log("Type Safety Examples (these would fail to compile)") 168 + console.log("=".repeat(60)) 169 + 170 + console.log(` 171 + 1. BRANDED TYPES prevent ID mixups: 172 + 173 + // This would NOT compile — different brand types! 174 + // const confused = createWorkOrder( 175 + // damId("dam-north"), // DamId, not WorkOrderId! 176 + // damNorth, 177 + // "Fix something" 178 + // ) 179 + // Error: Type 'DamId' is not assignable to 'WorkOrderId' 180 + 181 + 2. TYPESTATE prevents invalid transitions: 182 + 183 + // Cannot complete work that was never started: 184 + // const badComplete = completeWork(damRepair, "Done!") 185 + // Error: Type 'PendingWork' is not assignable to 'InProgressWork' 186 + 187 + // Cannot start work that's already in progress: 188 + // const doubleStart = startWork(damInProgress, "Fox") 189 + // Error: Type 'InProgressWork' is not assignable to 'PendingWork' 190 + 191 + // Cannot start completed work: 192 + // const restartCompleted = startWork(damCompleted, "Rabbit") 193 + // Error: Type 'CompletedWork' is not assignable to 'PendingWork' 194 + 195 + 3. LITERAL TYPES catch typos: 196 + 197 + // This would NOT compile — typo in state: 198 + // const badWork: PendingWork = { 199 + // _state: "Pendnig", // Typo! 200 + // ... 201 + // } 202 + // Error: Type '"Pendnig"' is not assignable to type '"Pending"' 203 + `) 204 + 205 + // ============================================================================= 206 + // Proper State Machine Flow 207 + // ============================================================================= 208 + 209 + console.log("=".repeat(60)) 210 + console.log("Proper State Machine Flow") 211 + console.log("=".repeat(60) + "\n") 212 + 213 + // Create multiple work orders and progress them 214 + const workOrders: WorkOrder[] = [] 215 + 216 + // Work order 1: Full lifecycle 217 + const path1 = createWorkOrder(workOrderId("wo-3"), pathId("path-main"), "Clear fallen branches") 218 + const path1Started = startWork(path1, "Fox") 219 + const path1Done = completeWork(path1Started, "Path cleared, branches moved to compost") 220 + workOrders.push(path1Done) 221 + 222 + // Work order 2: Still in progress 223 + const burrow1 = createWorkOrder(workOrderId("wo-4"), burrowId("burrow-rabbit"), "Annual inspection") 224 + const burrow1Started = startWork(burrow1, "Owl") 225 + workOrders.push(burrow1Started) 226 + 227 + // Work order 3: Still pending 228 + const dam2 = createWorkOrder(workOrderId("wo-5"), damId("dam-south"), "Reinforce foundation") 229 + workOrders.push(dam2) 230 + 231 + console.log("All work orders:\n") 232 + workOrders.forEach((work) => { 233 + console.log(` ${describeWorkOrder(work)}`) 234 + }) 235 + 236 + // Summary by state using pattern matching 237 + console.log("\n" + "-".repeat(40)) 238 + console.log("Summary by state:\n") 239 + 240 + const countByState = (work: WorkOrder, acc: { pending: number; inProgress: number; completed: number }) => { 241 + switch (work._state) { 242 + case "Pending": 243 + return { ...acc, pending: acc.pending + 1 } 244 + case "InProgress": 245 + return { ...acc, inProgress: acc.inProgress + 1 } 246 + case "Completed": 247 + return { ...acc, completed: acc.completed + 1 } 248 + } 249 + } 250 + 251 + const counts = workOrders.reduce( 252 + (acc, work) => countByState(work, acc), 253 + { pending: 0, inProgress: 0, completed: 0 }, 254 + ) 255 + 256 + console.log(` Pending: ${counts.pending}`) 257 + console.log(` In Progress: ${counts.inProgress}`) 258 + console.log(` Completed: ${counts.completed}`) 259 + 260 + console.log("\n=== End of Branded Types + Typestate Demo ===")
+316
examples/stories/beavers-big-system/02-adt-matching.ts
··· 1 + /** 2 + * Beaver's Big System — Part 2: ADTs + Pattern Matching 3 + * ====================================================== 4 + * 5 + * This example accompanies the story at: 6 + * docs/guides/stories/beavers-big-system/02-the-categorization.md 7 + * 8 + * It demonstrates how discriminated unions (ADTs) model different work types, 9 + * and how exhaustive pattern matching ensures you handle every case. 10 + * 11 + * Run with: bun examples/stories/beavers-big-system/02-adt-matching.ts 12 + */ 13 + 14 + import { type Branded, brand, match, matchOr } from "../../../src/index" 15 + 16 + // ============================================================================= 17 + // Branded IDs — Distinct types for each infrastructure category 18 + // ============================================================================= 19 + 20 + type WorkOrderId = Branded<string, "WorkOrderId"> 21 + type DamId = Branded<string, "DamId"> 22 + type BridgeId = Branded<string, "BridgeId"> 23 + type PathId = Branded<string, "PathId"> 24 + type BurrowId = Branded<string, "BurrowId"> 25 + 26 + const workOrderId = (id: string): WorkOrderId => brand(id) 27 + const damId = (id: string): DamId => brand(id) 28 + const bridgeId = (id: string): BridgeId => brand(id) 29 + const pathId = (id: string): PathId => brand(id) 30 + const burrowId = (id: string): BurrowId => brand(id) 31 + 32 + // ============================================================================= 33 + // Work Detail Types — Each kind of work has its own shape 34 + // ============================================================================= 35 + 36 + type DamRepair = { 37 + readonly _tag: "DamRepair" 38 + readonly damId: DamId 39 + readonly waterLevel: number 40 + readonly repairType: "structural" | "spillway" | "foundation" 41 + } 42 + 43 + type BridgeWork = { 44 + readonly _tag: "BridgeWork" 45 + readonly bridgeId: BridgeId 46 + readonly weightCapacity: number 47 + readonly inspectionRequired: boolean 48 + } 49 + 50 + type PathClearing = { 51 + readonly _tag: "PathClearing" 52 + readonly pathId: PathId 53 + readonly equipment: "saw" | "shovel" | "rake" 54 + readonly estimatedLength: number 55 + } 56 + 57 + type BurrowInspection = { 58 + readonly _tag: "BurrowInspection" 59 + readonly burrowId: BurrowId 60 + readonly size: "small" | "medium" | "large" 61 + readonly occupant: string 62 + } 63 + 64 + // The discriminated union of all work types 65 + type WorkDetails = 66 + | DamRepair 67 + | BridgeWork 68 + | PathClearing 69 + | BurrowInspection 70 + 71 + // ============================================================================= 72 + // Work Order combining ID with Details 73 + // ============================================================================= 74 + 75 + type WorkOrder = { 76 + readonly id: WorkOrderId 77 + readonly details: WorkDetails 78 + readonly createdAt: number 79 + } 80 + 81 + const createWorkOrder = ( 82 + id: WorkOrderId, 83 + details: WorkDetails, 84 + ): WorkOrder => ({ 85 + id, 86 + details, 87 + createdAt: Date.now(), 88 + }) 89 + 90 + // ============================================================================= 91 + // Functions using Exhaustive Pattern Matching 92 + // ============================================================================= 93 + 94 + /** 95 + * Estimate hours needed for work. 96 + * The match() function requires handlers for ALL variants. 97 + * If you forget one, TypeScript gives a compile error. 98 + */ 99 + const estimateHours = (details: WorkDetails): number => 100 + match(details)({ 101 + DamRepair: ({ waterLevel, repairType }) => 102 + repairType === "foundation" ? 12 : waterLevel > 5 ? 8 : 4, 103 + BridgeWork: ({ inspectionRequired, weightCapacity }) => 104 + inspectionRequired ? 6 : weightCapacity < 1000 ? 2 : 4, 105 + PathClearing: ({ estimatedLength, equipment }) => 106 + equipment === "saw" 107 + ? Math.ceil(estimatedLength / 50) 108 + : Math.ceil(estimatedLength / 100), 109 + BurrowInspection: ({ size }) => 110 + size === "large" ? 3 : size === "medium" ? 2 : 1, 111 + }) 112 + 113 + /** 114 + * Get a human-readable description of the work. 115 + */ 116 + const describeWork = (details: WorkDetails): string => 117 + match(details)({ 118 + DamRepair: ({ repairType, waterLevel }) => 119 + `Dam ${repairType} repair (water level: ${waterLevel}m)`, 120 + BridgeWork: ({ inspectionRequired, weightCapacity }) => 121 + inspectionRequired 122 + ? `Bridge inspection and repair (capacity: ${weightCapacity}kg)` 123 + : `Bridge maintenance (capacity: ${weightCapacity}kg)`, 124 + PathClearing: ({ equipment, estimatedLength }) => 125 + `Clear ${estimatedLength}m of path using ${equipment}`, 126 + BurrowInspection: ({ occupant, size }) => 127 + `Inspect ${size} burrow (${occupant}'s home)`, 128 + }) 129 + 130 + /** 131 + * Check if work requires special equipment. 132 + */ 133 + const requiresEquipment = (details: WorkDetails): boolean => 134 + match(details)({ 135 + DamRepair: () => true, 136 + BridgeWork: () => true, 137 + PathClearing: () => true, 138 + BurrowInspection: () => false, 139 + }) 140 + 141 + /** 142 + * Get priority level for scheduling. 143 + */ 144 + const getPriority = (details: WorkDetails): "critical" | "high" | "normal" | "low" => 145 + match(details)({ 146 + DamRepair: ({ repairType }) => 147 + repairType === "foundation" ? "critical" : "high", 148 + BridgeWork: ({ inspectionRequired }) => 149 + inspectionRequired ? "high" : "normal", 150 + PathClearing: () => "normal", 151 + BurrowInspection: () => "low", 152 + }) 153 + 154 + // ============================================================================= 155 + // Partial Matching with matchOr 156 + // ============================================================================= 157 + 158 + /** 159 + * Check if work is relevant to Rabbit. 160 + * Using matchOr() for partial matching — only handle cases we care about. 161 + */ 162 + const isRabbitRelevant = (details: WorkDetails): boolean => 163 + matchOr(false)(details)({ 164 + BurrowInspection: ({ occupant }) => occupant === "Rabbit", 165 + }) 166 + 167 + /** 168 + * Get water-related info (only applicable to some work types). 169 + */ 170 + const getWaterInfo = (details: WorkDetails): string => 171 + matchOr("N/A")(details)({ 172 + DamRepair: ({ waterLevel }) => `Water level: ${waterLevel}m`, 173 + }) 174 + 175 + // ============================================================================= 176 + // Demo 177 + // ============================================================================= 178 + 179 + console.log("=== Beaver's Big System: ADTs + Pattern Matching ===\n") 180 + 181 + // Create work orders with different detail types 182 + const workOrders: WorkOrder[] = [ 183 + createWorkOrder(workOrderId("wo-1"), { 184 + _tag: "DamRepair", 185 + damId: damId("dam-north"), 186 + waterLevel: 3.2, 187 + repairType: "spillway", 188 + }), 189 + createWorkOrder(workOrderId("wo-2"), { 190 + _tag: "BridgeWork", 191 + bridgeId: bridgeId("bridge-east"), 192 + weightCapacity: 500, 193 + inspectionRequired: true, 194 + }), 195 + createWorkOrder(workOrderId("wo-3"), { 196 + _tag: "PathClearing", 197 + pathId: pathId("path-main"), 198 + equipment: "saw", 199 + estimatedLength: 200, 200 + }), 201 + createWorkOrder(workOrderId("wo-4"), { 202 + _tag: "BurrowInspection", 203 + burrowId: burrowId("burrow-rabbit"), 204 + size: "medium", 205 + occupant: "Rabbit", 206 + }), 207 + createWorkOrder(workOrderId("wo-5"), { 208 + _tag: "DamRepair", 209 + damId: damId("dam-south"), 210 + waterLevel: 6.5, 211 + repairType: "foundation", 212 + }), 213 + ] 214 + 215 + console.log("Work Orders:\n") 216 + console.log("-".repeat(70)) 217 + 218 + workOrders.forEach((order) => { 219 + const { details } = order 220 + console.log(`ID: ${order.id}`) 221 + console.log(` Type: ${details._tag}`) 222 + console.log(` Description: ${describeWork(details)}`) 223 + console.log(` Estimated hours: ${estimateHours(details)}`) 224 + console.log(` Priority: ${getPriority(details)}`) 225 + console.log(` Requires equipment: ${requiresEquipment(details) ? "Yes" : "No"}`) 226 + console.log(` Water info: ${getWaterInfo(details)}`) 227 + console.log(` Rabbit relevant: ${isRabbitRelevant(details) ? "Yes" : "No"}`) 228 + console.log("-".repeat(70)) 229 + }) 230 + 231 + // ============================================================================= 232 + // Type Safety Demonstrations 233 + // ============================================================================= 234 + 235 + console.log("\n" + "=".repeat(60)) 236 + console.log("Type Safety: Exhaustive Matching") 237 + console.log("=".repeat(60)) 238 + 239 + console.log(` 240 + When you add a new work type, EVERY match() breaks until handled. 241 + 242 + Example: Add TreeTrimming to the union: 243 + 244 + type TreeTrimming = { 245 + readonly _tag: "TreeTrimming" 246 + readonly treeId: TreeId 247 + readonly height: number 248 + readonly species: string 249 + } 250 + 251 + type WorkDetails = ... | TreeTrimming 252 + 253 + Now estimateHours() fails to compile: 254 + 255 + const estimateHours = (details: WorkDetails): number => 256 + match(details)({ 257 + DamRepair: ..., 258 + BridgeWork: ..., 259 + PathClearing: ..., 260 + BurrowInspection: ..., 261 + // Error: Property 'TreeTrimming' is missing in type... 262 + }) 263 + 264 + The compiler guides you to every place that needs updating! 265 + `) 266 + 267 + // ============================================================================= 268 + // Grouping by Type 269 + // ============================================================================= 270 + 271 + console.log("=".repeat(60)) 272 + console.log("Grouping by Work Type") 273 + console.log("=".repeat(60) + "\n") 274 + 275 + const groupByType = (orders: WorkOrder[]): Record<string, WorkOrder[]> => 276 + orders.reduce( 277 + (acc, order) => ({ 278 + ...acc, 279 + [order.details._tag]: [...(acc[order.details._tag] ?? []), order], 280 + }), 281 + {} as Record<string, WorkOrder[]>, 282 + ) 283 + 284 + const grouped = groupByType(workOrders) 285 + 286 + Object.entries(grouped).forEach(([type, orders]) => { 287 + console.log(`${type}: ${orders.length} order(s)`) 288 + orders.forEach((o) => console.log(` - ${o.id}: ${describeWork(o.details)}`)) 289 + }) 290 + 291 + // ============================================================================= 292 + // Total Hours by Priority 293 + // ============================================================================= 294 + 295 + console.log("\n" + "-".repeat(40)) 296 + console.log("Hours by Priority:\n") 297 + 298 + const hoursByPriority = workOrders.reduce( 299 + (acc, order) => { 300 + const priority = getPriority(order.details) 301 + const hours = estimateHours(order.details) 302 + return { ...acc, [priority]: (acc[priority] ?? 0) + hours } 303 + }, 304 + {} as Record<string, number>, 305 + ) 306 + 307 + Object.entries(hoursByPriority) 308 + .sort(([a], [b]) => { 309 + const order = ["critical", "high", "normal", "low"] 310 + return order.indexOf(a) - order.indexOf(b) 311 + }) 312 + .forEach(([priority, hours]) => { 313 + console.log(` ${priority}: ${hours} hours`) 314 + }) 315 + 316 + console.log("\n=== End of ADTs + Pattern Matching Demo ===")
+350
examples/stories/beavers-big-system/03-tracked-arrays.ts
··· 1 + /** 2 + * Beaver's Big System — Part 3: Tracked Arrays + Refinements 3 + * =========================================================== 4 + * 5 + * This example accompanies the story at: 6 + * docs/guides/stories/beavers-big-system/03-the-queue.md 7 + * 8 + * It demonstrates how phantom types track array properties like NonEmpty 9 + * and Sorted, and how refinements validate values at creation time. 10 + * 11 + * Run with: bun examples/stories/beavers-big-system/03-tracked-arrays.ts 12 + */ 13 + 14 + import { 15 + type Branded, 16 + type Option, 17 + type Arr, 18 + type NonEmpty, 19 + type Sorted, 20 + brand, 21 + some, 22 + none, 23 + match, 24 + arr, 25 + nonEmpty, 26 + sortBy, 27 + head, 28 + pipe, 29 + } from "../../../src/index" 30 + 31 + // ============================================================================= 32 + // Priority — A refined type for valid priority values 33 + // ============================================================================= 34 + 35 + type Priority = Branded<number, "Priority"> 36 + 37 + /** 38 + * Create a valid priority. 39 + * Returns Option because validation can fail. 40 + * Priority must be a non-negative integer. 41 + */ 42 + const priority = (n: number): Option<Priority> => 43 + Number.isInteger(n) && n >= 0 ? some(brand(n)) : none 44 + 45 + // ============================================================================= 46 + // Work Order (simplified for queue demonstration) 47 + // ============================================================================= 48 + 49 + type WorkOrderId = Branded<string, "WorkOrderId"> 50 + 51 + type WorkOrder = { 52 + readonly id: WorkOrderId 53 + readonly description: string 54 + readonly priority: Priority 55 + } 56 + 57 + const workOrder = ( 58 + id: string, 59 + description: string, 60 + p: Priority, 61 + ): WorkOrder => ({ 62 + id: brand(id), 63 + description, 64 + priority: p, 65 + }) 66 + 67 + // ============================================================================= 68 + // Queue Types — Phantom types track properties 69 + // ============================================================================= 70 + 71 + // Basic queue — we know nothing about it 72 + type WorkQueue = Arr<WorkOrder> 73 + 74 + // Queue that's guaranteed non-empty — safe to call head() 75 + type NonEmptyQueue = Arr<WorkOrder, NonEmpty> 76 + 77 + // Queue that's guaranteed sorted by priority 78 + type SortedQueue = Arr<WorkOrder, Sorted> 79 + 80 + // Queue that's both non-empty AND sorted — ready to process 81 + type ReadyQueue = Arr<WorkOrder, NonEmpty | Sorted> 82 + 83 + // ============================================================================= 84 + // Queue Operations 85 + // ============================================================================= 86 + 87 + /** 88 + * Sort queue by priority (lower number = higher priority). 89 + * Returns a Sorted array — the type carries the proof. 90 + */ 91 + const sortByPriority = sortBy<WorkOrder, never>( 92 + (work) => work.priority as number, 93 + ) 94 + 95 + /** 96 + * Add work to a sorted queue and maintain sort order. 97 + * Returns NonEmpty | Sorted because we're adding an element. 98 + */ 99 + const addToQueue = ( 100 + queue: SortedQueue, 101 + work: WorkOrder, 102 + ): ReadyQueue => 103 + sortByPriority(arr([...queue, work])) as ReadyQueue 104 + 105 + /** 106 + * Get next work from a non-empty queue. 107 + * REQUIRES NonEmpty — compiler won't let you call this on a possibly-empty queue. 108 + */ 109 + const getNext = (queue: NonEmptyQueue): WorkOrder => head(queue) 110 + 111 + /** 112 + * Try to get next work, handling the possibly-empty case. 113 + * Returns Option — None if queue is empty. 114 + */ 115 + const tryGetNext = (queue: WorkQueue): Option<WorkOrder> => 116 + pipe(nonEmpty(queue), (opt) => 117 + match(opt)({ 118 + Some: ({ value }) => some(head(value)), 119 + None: () => none, 120 + }), 121 + ) 122 + 123 + /** 124 + * Process work from a possibly-empty queue. 125 + * Demonstrates safe handling of the NonEmpty requirement. 126 + */ 127 + const processQueue = ( 128 + queue: WorkQueue, 129 + onWork: (work: WorkOrder) => void, 130 + onEmpty: () => void, 131 + ): void => 132 + match(nonEmpty(queue))({ 133 + Some: ({ value }) => onWork(head(value)), 134 + None: onEmpty, 135 + }) 136 + 137 + // ============================================================================= 138 + // Demo 139 + // ============================================================================= 140 + 141 + console.log("=== Beaver's Big System: Tracked Arrays + Refinements ===\n") 142 + 143 + // ============================================================================= 144 + // Part 1: Priority Refinement 145 + // ============================================================================= 146 + 147 + console.log("--- Priority Validation ---\n") 148 + 149 + const testPriorities = [1, 5, 3, 0, -1, 2.5, 100] 150 + 151 + testPriorities.forEach((n) => { 152 + const result = priority(n) 153 + match(result)({ 154 + Some: ({ value }) => console.log(` priority(${n}) = Valid: ${value}`), 155 + None: () => console.log(` priority(${n}) = Invalid (must be non-negative integer)`), 156 + }) 157 + }) 158 + 159 + // ============================================================================= 160 + // Part 2: Building Work Orders with Valid Priorities 161 + // ============================================================================= 162 + 163 + console.log("\n--- Creating Work Orders ---\n") 164 + 165 + // Helper to create work order only if priority is valid 166 + const createWorkOrder = ( 167 + id: string, 168 + desc: string, 169 + p: number, 170 + ): Option<WorkOrder> => 171 + match(priority(p))({ 172 + Some: ({ value }) => some(workOrder(id, desc, value)), 173 + None: () => none, 174 + }) 175 + 176 + // Create some work orders 177 + const maybeOrders = [ 178 + createWorkOrder("wo-1", "Fix spillway", 1), 179 + createWorkOrder("wo-2", "Inspect bridge", 5), 180 + createWorkOrder("wo-3", "Clear path", 3), 181 + createWorkOrder("wo-4", "Urgent repair", -1), // Invalid! 182 + createWorkOrder("wo-5", "Check burrow", 2), 183 + ] 184 + 185 + console.log("Attempting to create work orders:") 186 + maybeOrders.forEach((result, i) => { 187 + match(result)({ 188 + Some: ({ value }) => 189 + console.log(` Order ${i + 1}: Created "${value.description}" (priority ${value.priority})`), 190 + None: () => console.log(` Order ${i + 1}: Rejected (invalid priority)`), 191 + }) 192 + }) 193 + 194 + // Collect only valid orders 195 + const validOrders: WorkOrder[] = maybeOrders.flatMap((opt) => 196 + match(opt)({ 197 + Some: ({ value }) => [value], 198 + None: () => [], 199 + }), 200 + ) 201 + 202 + console.log(`\nValid orders collected: ${validOrders.length}`) 203 + 204 + // ============================================================================= 205 + // Part 3: Building a Sorted Queue 206 + // ============================================================================= 207 + 208 + console.log("\n--- Building Sorted Queue ---\n") 209 + 210 + // Start with an empty sorted queue 211 + const emptyQueue: SortedQueue = arr([]) as SortedQueue 212 + 213 + // Add work orders one by one — each addition maintains sort order 214 + const queue1 = addToQueue(emptyQueue, validOrders[0]!) 215 + const queue2 = addToQueue(queue1, validOrders[1]!) 216 + const queue3 = addToQueue(queue2, validOrders[2]!) 217 + const fullQueue = addToQueue(queue3, validOrders[3]!) 218 + 219 + console.log("Queue after adding all valid work orders (sorted by priority):") 220 + fullQueue.forEach((work, i) => { 221 + console.log(` ${i + 1}. [Priority ${work.priority}] ${work.description}`) 222 + }) 223 + 224 + // ============================================================================= 225 + // Part 4: Safe Queue Access 226 + // ============================================================================= 227 + 228 + console.log("\n--- Safe Queue Access ---\n") 229 + 230 + // fullQueue is ReadyQueue (NonEmpty | Sorted), so we can safely call getNext 231 + console.log("Getting next work from full queue (guaranteed safe):") 232 + const nextWork = getNext(fullQueue) 233 + console.log(` Next: "${nextWork.description}" (priority ${nextWork.priority})`) 234 + 235 + // For a possibly-empty queue, we must use tryGetNext or processQueue 236 + console.log("\nHandling possibly-empty queue:") 237 + 238 + const maybeEmptyQueue: WorkQueue = arr([]) 239 + 240 + processQueue( 241 + maybeEmptyQueue, 242 + (work) => console.log(` Got work: ${work.description}`), 243 + () => console.log(" Queue is empty - no work to process"), 244 + ) 245 + 246 + processQueue( 247 + fullQueue, 248 + (work) => console.log(` Got work: ${work.description}`), 249 + () => console.log(" Queue is empty"), 250 + ) 251 + 252 + // ============================================================================= 253 + // Type Safety Demonstrations 254 + // ============================================================================= 255 + 256 + console.log("\n" + "=".repeat(60)) 257 + console.log("Type Safety: Phantom Types") 258 + console.log("=".repeat(60)) 259 + 260 + console.log(` 261 + 1. Cannot call head() on a possibly-empty array: 262 + 263 + const queue: Arr<WorkOrder> = arr([]) 264 + 265 + // This FAILS to compile! 266 + // const next = head(queue) 267 + // Error: Argument of type 'Arr<WorkOrder>' is not assignable 268 + // to parameter of type 'Arr<WorkOrder, NonEmpty>' 269 + 270 + 2. Must prove non-emptiness first: 271 + 272 + match(nonEmpty(queue))({ 273 + Some: ({ value }) => { 274 + // Inside here, 'value' is Arr<WorkOrder, NonEmpty> 275 + const next = head(value) // Now it's safe! 276 + }, 277 + None: () => { 278 + // Handle empty case 279 + }, 280 + }) 281 + 282 + 3. Operations track property changes: 283 + 284 + // filter() removes NonEmpty (might filter out all elements) 285 + const filtered = filter(pred)(nonEmptyQueue) 286 + // Type: Arr<WorkOrder, never> — NonEmpty is gone! 287 + 288 + // sortBy() adds Sorted 289 + const sorted = sortByPriority(queue) 290 + // Type: Arr<WorkOrder, Sorted> 291 + `) 292 + 293 + // ============================================================================= 294 + // Full Workflow Example 295 + // ============================================================================= 296 + 297 + console.log("=".repeat(60)) 298 + console.log("Full Workflow: Priority Queue Processing") 299 + console.log("=".repeat(60) + "\n") 300 + 301 + // Simulate processing a work queue 302 + const processWorkQueue = (queue: WorkQueue): void => { 303 + let remaining = queue 304 + let processed = 0 305 + 306 + const loop = (): void => { 307 + match(nonEmpty(remaining))({ 308 + Some: ({ value: nonEmptyRemaining }) => { 309 + const work = head(nonEmptyRemaining) 310 + console.log(` Processing: "${work.description}" (priority ${work.priority})`) 311 + processed++ 312 + 313 + // Remove the first element (creates a possibly-empty array) 314 + remaining = arr(nonEmptyRemaining.slice(1)) 315 + loop() 316 + }, 317 + None: () => { 318 + console.log(`\n Done! Processed ${processed} work orders.`) 319 + }, 320 + }) 321 + } 322 + 323 + loop() 324 + } 325 + 326 + console.log("Processing all work in priority order:\n") 327 + processWorkQueue(fullQueue) 328 + 329 + // ============================================================================= 330 + // Edge Cases 331 + // ============================================================================= 332 + 333 + console.log("\n" + "-".repeat(40)) 334 + console.log("Edge Cases:\n") 335 + 336 + // Trying to process an empty queue 337 + console.log("1. Empty queue:") 338 + processWorkQueue(arr([])) 339 + 340 + // Single item queue 341 + console.log("\n2. Single item queue:") 342 + match(createWorkOrder("wo-solo", "Solo task", 1))({ 343 + Some: ({ value }) => { 344 + const singleQueue = addToQueue(emptyQueue, value) 345 + processWorkQueue(singleQueue) 346 + }, 347 + None: () => console.log(" Failed to create work order"), 348 + }) 349 + 350 + console.log("\n=== End of Tracked Arrays + Refinements Demo ===")