Challenge 2: Validate Incoming Socket Data — Possible Solution ==================================================================== Option A — using zod: import { z } from "zod"; const ClientMessageSchema = z.discriminatedUnion("type", [ z.object({ type: z.literal("move"), playerId: z.string(), x: z.number(), y: z.number() }), z.object({ type: z.literal("chat"), playerId: z.string(), text: z.string().min(1) }), z.object({ type: z.literal("leave"), playerId: z.string() }), ]); type ClientMessage = z.infer; function handleRawMessage(raw: string, send: (data: unknown) => void): void { let parsed: unknown; try { parsed = JSON.parse(raw); } catch { send({ type: "error", message: "Malformed JSON" }); return; } const result = ClientMessageSchema.safeParse(parsed); if (!result.success) { send({ type: "error", message: "Invalid message shape" }); return; } handleMessage(result.data); // fully typed AND validated ClientMessage } function handleMessage(message: ClientMessage): void { switch (message.type) { case "move": console.log(`${message.playerId} moved to (${message.x}, ${message.y})`); break; case "chat": console.log(`${message.playerId}: ${message.text}`); break; case "leave": console.log(`${message.playerId} left`); break; } } Option B — hand-rolled type guard (no dependency): interface MoveMessage { type: "move"; playerId: string; x: number; y: number } interface ChatMessage { type: "chat"; playerId: string; text: string } interface LeaveMessage { type: "leave"; playerId: string } type ClientMessage = MoveMessage | ChatMessage | LeaveMessage; function isClientMessage(value: unknown): value is ClientMessage { if (typeof value !== "object" || value === null) return false; const v = value as Record; if (typeof v.playerId !== "string") return false; switch (v.type) { case "move": return typeof v.x === "number" && typeof v.y === "number"; case "chat": return typeof v.text === "string" && v.text.length > 0; case "leave": return true; default: return false; } } WHY THIS WORKS -------------- - z.discriminatedUnion mirrors the ClientMessage type exactly, so a message missing a required field (or with the wrong type for a field) is rejected before handleMessage ever sees it — not silently coerced or ignored. - The JSON.parse() call is wrapped in a try/catch separately from the schema check, because malformed JSON throws before validation ever gets a chance to run. - Option B shows the same guarantee without a library: the type predicate checks the discriminant first, then validates exactly the fields that variant requires, narrowing `value` to ClientMessage only if every check passes.