SOLUTION: Challenge 2 - Type Predicates ======================================== Challenge: Write custom type guard functions: isString(), isArray(), isRecord(). Test them with discriminated unions. --- SOLUTION: // 1. isString() — simple type predicate function isString(value: unknown): value is string { return typeof value === "string"; } // Test const test1: unknown = "hello"; if (isString(test1)) { console.log(test1.toUpperCase()); // ✅ TypeScript knows it's string } --- // 2. isArray() — generic type predicate function isArray(value: unknown): value is T[] { return Array.isArray(value); } const test2: unknown = [1, 2, 3]; if (isArray(test2)) { console.log(test2.map(n => n * 2)); // ✅ TypeScript knows it's number[] } --- // 3. isRecord() — check if it's an object with properties function isRecord>( value: unknown ): value is T { return ( typeof value === "object" && value !== null && !Array.isArray(value) ); } interface User { name: string; email: string; } const test3: unknown = { name: "Alice", email: "alice@example.com" }; if (isRecord(test3)) { console.log(test3.name); // ✅ TypeScript knows it's User } --- // ADVANCED: Discriminated Union with Type Predicates type Result = | { status: "success"; data: T } | { status: "error"; error: string } | { status: "loading" }; function isSuccess(result: Result): result is { status: "success"; data: T } { return result.status === "success"; } function isError(result: Result): result is { status: "error"; error: string } { return result.status === "error"; } function isLoading(result: Result): result is { status: "loading" } { return result.status === "loading"; } // Usage const response: Result = { status: "success", data: 42 }; if (isSuccess(response)) { console.log(response.data); // ✅ number } else if (isError(response)) { console.log(response.error); // ✅ string } else if (isLoading(response)) { console.log("Loading..."); } --- // ADVANCED: Array Guard with Type Assertion function isStringArray(value: unknown): value is string[] { return Array.isArray(value) && value.every(item => typeof item === "string"); } const test4: unknown = ["a", "b", "c"]; if (isStringArray(test4)) { const uppercase = test4.map(s => s.toUpperCase()); // ✅ Safe console.log(uppercase); } --- // ADVANCED: Object Shape Guard interface Config { apiUrl: string; timeout: number; retries: number; } function isConfig(value: unknown): value is Config { return ( typeof value === "object" && value !== null && "apiUrl" in value && "timeout" in value && "retries" in value && typeof (value as any).apiUrl === "string" && typeof (value as any).timeout === "number" && typeof (value as any).retries === "number" ); } const config: unknown = { apiUrl: "https://api.example.com", timeout: 5000, retries: 3 }; if (isConfig(config)) { console.log(`Connecting to ${config.apiUrl}...`); // ✅ Type-safe } --- EXPLANATION: BASIC TYPE PREDICATE: function isString(value: unknown): value is string { return typeof value === "string"; } The return type "value is string" tells TypeScript: "If this function returns true, treat value as string" Usage: const x: unknown = "hello"; if (isString(x)) { x.toUpperCase(); // ✅ Now TypeScript knows x is string } GENERIC TYPE PREDICATE: function isArray(value: unknown): value is T[] { return Array.isArray(value); } Same pattern but with a generic type parameter T. When you call isArray(value), TypeScript narrows to number[]. DISCRIMINATED UNION: type Result = | { status: "success"; data: T } | { status: "error"; error: string } | { status: "loading" }; Each variant has a unique status value. Type predicates check the status and narrow to the right variant. Instead of checking status in an if-else: if (response.status === "success") { ... } You can use the type predicate: if (isSuccess(response)) { ... } Both work, but type predicates are more composable. OBJECT SHAPE GUARD: To ensure an object has all required properties with correct types: function isConfig(value: unknown): value is Config { return ( typeof value === "object" && value !== null && "apiUrl" in value && typeof (value as any).apiUrl === "string" && // ... check each property ); } This is runtime validation + type narrowing combined. WHY TYPE PREDICATES MATTER: 1. Bridge runtime and compile-time: validate at runtime, narrow at compile-time 2. Reusable: define once, use many times 3. Composable: chain multiple predicates 4. Clear intent: makes narrowing explicit 5. DRY: encode the logic once, not repeated in conditionals --- REAL-WORLD PATTERNS: TypeScript and libraries use this everywhere: - React: isValidElement() - Lodash: isArray(), isObject(), isString() - Axios: isAxiosError() - zod (validation): guards that narrow types Mastering type predicates unlocks type-safe runtime code.