SOLUTION: Challenge 3 - Compose Utilities =========================================== Challenge: Use Pick, Omit, Partial, Readonly together to build derived types. Create safe API response types from your domain types. --- SOLUTION: // Domain types interface User { id: number; name: string; email: string; password: string; role: "admin" | "user" | "guest"; createdAt: Date; updatedAt: Date; } interface Post { id: number; title: string; content: string; authorId: number; published: boolean; createdAt: Date; updatedAt: Date; } --- // API Response Types — built from domain types // 1. Safe User (no password) type SafeUser = Omit; // { id: number; name: string; email: string; role: ...; createdAt: Date; ... } // 2. User Preview (safe + minimal) type UserPreview = Pick; // { id: number; name: string; email: string; } // 3. User Create Request (no id/dates, all required) type CreateUserRequest = Omit & { password: string; }; // { name: string; email: string; role: ...; password: string; } // 4. User Update Request (partial + safe) type UpdateUserRequest = Partial>; // { name?: string; email?: string; role?: ...; } // 5. Read-only API Response (immutable) type UserApiResponse = Readonly; // { readonly id: number; readonly name: string; ... } --- // API Response Wrapper interface ApiResponse { status: "success" | "error"; data?: T; error?: { code: string; message: string; }; timestamp: Date; } // Typed API responses type GetUserResponse = ApiResponse; type ListUsersResponse = ApiResponse>; type CreateUserResponse = ApiResponse; type UpdateUserResponse = ApiResponse; --- // Usage Example async function getUser(id: number): Promise { return { status: "success", data: { id: 1, name: "Alice", email: "alice@example.com", role: "user", createdAt: new Date(), updatedAt: new Date() } as readonly any, timestamp: new Date() }; } async function updateUser( id: number, updates: UpdateUserRequest ): Promise { return { status: "success", data: { id, name: updates.name || "Alice", email: updates.email || "alice@example.com", role: updates.role || "user", createdAt: new Date(), updatedAt: new Date() } as readonly any, timestamp: new Date() }; } async function createUser( request: CreateUserRequest ): Promise { return { status: "success", data: { id: 1, ...request, createdAt: new Date(), updatedAt: new Date() } as readonly any, timestamp: new Date() }; } --- // Usage in application // Fetching const userResponse = await getUser(1); if (userResponse.status === "success" && userResponse.data) { console.log(userResponse.data.name); // ✅ Safe, no password // userResponse.data.password; // ❌ Error: property doesn't exist } // Creating const createReq: CreateUserRequest = { name: "Bob", email: "bob@example.com", role: "user", password: "secret123" }; await createUser(createReq); // ✅ Type-safe // Updating const updateReq: UpdateUserRequest = { name: "Charlie" // email is optional, role is optional }; await updateUser(1, updateReq); // ✅ Type-safe, partial --- EXPLANATION: COMPOSITION CHAIN: SafeUser = Omit → Remove sensitive field UserPreview = Pick → Select only needed fields CreateUserRequest = Omit & { password: string } → No internal fields, but require password for new accounts UpdateUserRequest = Partial> → No internal fields, all are optional (update only what changed) UserApiResponse = Readonly → Safe AND immutable (can't modify returned data) BENEFITS: 1. Type Safety: Each operation has exactly the right type 2. Security: Password removed from responses 3. Flexibility: Create, update, and get have different requirements 4. Reusability: Compose from domain types, not duplicating definitions 5. Maintainability: Change User once, all derived types update automatically PATTERN: Always derive API types from domain types Never manually duplicate type definitions. --- ADVANCED: Generic API Response Builder You can build a helper that abstracts the pattern: type SafeResponse = Omit; type CreateRequest = Omit; type UpdateRequest = Partial>; type ApiReadResponse = Readonly; // Usage type SafeUserResponse = ApiReadResponse>; type UserCreateRequest = CreateRequest; type UserUpdateRequest = UpdateRequest; This scales to any domain type without repetition. --- REAL-WORLD EXAMPLE: NestJS (framework) does this automatically with decorators: @Controller("users") export class UserController { @Get(":id") async getUser(@Param("id") id: number): Promise { // DTO (Data Transfer Object) is a composed/filtered version of User } @Post() async createUser(@Body() createUserDto: CreateUserDto): Promise { // CreateUserDto is User with only create-relevant fields } @Patch(":id") async updateUser( @Param("id") id: number, @Body() updateUserDto: UpdateUserDto ): Promise { // UpdateUserDto is User with all fields optional } } The pattern: separate types for read (safe), create (required fields), update (partial fields). Mastering composition unlocks professional-grade TypeScript.