SOLUTION: Challenge 1 - Generic with Constraints ================================================= Challenge: Write a function that takes an object and a key, and returns the value. Use constraints to ensure the key exists on the object. Bonus: make the return type exactly match the property type. --- SOLUTION: // Basic version: ensure the key exists function getValue(obj: T, key: K): T[K] { return obj[key]; } // Test it interface User { id: number; name: string; email: string; } const user: User = { id: 1, name: "Alice", email: "alice@example.com" }; const id = getValue(user, "id"); // type: number const name = getValue(user, "name"); // type: string // getValue(user, "phone"); // ❌ Error: "phone" is not a key --- EXPLANATION: 1. GENERIC PARAMETERS: - T = the object type - K = the key we're accessing 2. CONSTRAINT (K extends keyof T): This is the magic. It says: "K must be a valid key from T." keyof T produces "id" | "name" | "email" for User. So K can only be one of those strings. 3. RETURN TYPE (T[K]): This accesses the type of the property at key K. - If K is "id", T[K] is number - If K is "name", T[K] is string - TypeScript automatically infers which one based on what key you pass 4. TYPE NARROWING: Because K is constrained to keyof T, the compiler knows the key exists. No runtime check needed — it's impossible to pass an invalid key at compile time. --- ADVANCED VERSION: With default parameter You could also write this with a default parameter value: function getValue(obj: T, key: K): T[K] { return obj[key]; } But this is rarely needed. The simple version above is more common. --- WHY THIS PATTERN MATTERS: 1. Type Safety: The compiler prevents typos in property names. 2. Autocomplete: Your IDE knows exactly which keys are valid. 3. Inference: The return type is automatically the correct type for that property. 4. No Runtime Overhead: All checked at compile time; the JavaScript is identical. This is the foundation of many TypeScript utility libraries.