SOLUTION: Challenge 3 - Result Type & Exhaustiveness ===================================================== Challenge: Implement a Result type. Add a discriminated union operation type. Use exhaustiveness checking to ensure all operations are handled. --- SOLUTION: // 1. Result type type Result = | { ok: true; value: T } | { ok: false; error: E }; // 2. Discriminated union for operations type Operation = | { type: "add"; a: number; b: number } | { type: "subtract"; a: number; b: number } | { type: "multiply"; a: number; b: number } | { type: "divide"; a: number; b: number }; // 3. Error type class CalculationError extends Error { constructor(public operation: string, message: string) { super(message); this.name = "CalculationError"; } } // 4. Execute operation with Result function executeOperation(op: Operation): Result { switch (op.type) { case "add": return { ok: true, value: op.a + op.b }; case "subtract": return { ok: true, value: op.a - op.b }; case "multiply": return { ok: true, value: op.a * op.b }; case "divide": if (op.b === 0) { return { ok: false, error: new CalculationError("divide", "Cannot divide by zero") }; } return { ok: true, value: op.a / op.b }; default: // Exhaustiveness check: if you forget a case, TypeScript errors here const _exhaustive: never = op; return _exhaustive; } } --- // 5. Handle results function handleCalculation(op: Operation): void { const result = executeOperation(op); if (result.ok) { console.log(`✅ ${op.type}(${(op as any).a}, ${(op as any).b}) = ${result.value}`); } else { console.error(`❌ ${op.type} failed: ${result.error.message}`); } } --- // 6. Test cases console.log("=== Testing Operation Results ===\n"); handleCalculation({ type: "add", a: 10, b: 5 }); handleCalculation({ type: "subtract", a: 10, b: 5 }); handleCalculation({ type: "multiply", a: 10, b: 5 }); handleCalculation({ type: "divide", a: 10, b: 5 }); handleCalculation({ type: "divide", a: 10, b: 0 }); --- ADVANCED: Chain Operations with Result // Chain multiple operations function chainOperations(ops: Operation[]): Result { let accumulator: Result = { ok: true, value: 0 }; for (const op of ops) { if (!accumulator.ok) { break; // Stop on first error } // Adjust operation to use accumulator const nextOp: Operation = { ...op, a: accumulator.value } as Operation; accumulator = executeOperation(nextOp); } return accumulator; } // Usage const result = chainOperations([ { type: "add", a: 0, b: 10 }, { type: "multiply", a: 0, b: 3 }, { type: "divide", a: 0, b: 2 } ]); if (result.ok) { console.log(`Final result: ${result.value}`); } else { console.error(`Calculation failed: ${result.error.message}`); } --- ADVANCED: Result Helpers // Map over success function mapResult( result: Result, fn: (value: T) => U ): Result { return result.ok ? { ok: true, value: fn(result.value) } : result; } // Flat-map (chain results) function flatMapResult( result: Result, fn: (value: T) => Result ): Result { return result.ok ? fn(result.value) : result; } // Usage const r = { ok: true, value: 10 } as Result; const r2 = mapResult(r, x => x * 2); // { ok: true, value: 20 } const r3 = flatMapResult(r2, x => x > 50 ? { ok: false, error: new Error("Too big") } : { ok: true, value: x }); --- EXPLANATION: RESULT TYPE: type Result = | { ok: true; value: T } | { ok: false; error: E }; Instead of throwing, return either: - Success: { ok: true, value: T } - Failure: { ok: false, error: E } Advantages over exceptions: 1. No try-catch needed (optional error handling) 2. Error is data (can be transformed, passed, logged) 3. Compiler forces you to handle errors (with exhaustiveness) 4. Can chain with map/flatMap DISCRIMINATED UNION: type Operation = | { type: "add"; a: number; b: number } | { type: "subtract"; a: number; b: number } | ... Each variant has a unique `type` field. TypeScript uses this to narrow types in switch: case "add": op is now { type: "add"; a: number; b: number } EXHAUSTIVENESS CHECKING: switch (op.type) { case "add": ... case "subtract": ... case "multiply": ... case "divide": ... default: const _exhaustive: never = op; return _exhaustive; } If you add a new Operation variant but forget to handle it: type Operation = ... | { type: "modulo"; a: number; b: number }; TypeScript will error: "Type 'modulo' is not assignable to type 'never'". This forces you to update all switch statements. --- COMPARISON: Exceptions vs Result Exceptions: try { const x = riskyOp(); } catch (e) { ... } Pros: Simple, imperative Cons: Unclear which operations throw, can be forgotten Result: const r = riskyOp(); if (!r.ok) { ... } Pros: Explicit, composable, chainable Cons: More verbose, requires pattern matching Choose based on context: - Exceptions: program state is broken (out of memory, null pointer) - Result: expected failures (validation, network, logic) --- REAL-WORLD USAGE: Rust uses Result as the primary error handling mechanism. Elm uses Result and Maybe for all error cases. TypeScript is gradually adopting Result pattern (zod, neverthrow libraries). This pattern is becoming standard in functional programming with TypeScript.