================================================================================ TYPESCRIPT FUNDAMENTALS - CHALLENGE 3 SOLUTION Chapter 8: Functions & Overloads Challenge: Generic Overload ================================================================================ PROBLEM: Create a wrapInArray function that takes a single value OR an array, and always returns an array. If passed a single value, wrap it. If passed an array, return it as-is. SOLUTION: ================================================================================ // Overload 1: Single value → wrapped in array function wrapInArray(value: T): T[]; // Overload 2: Array → return as-is function wrapInArray(value: T[]): T[]; // Implementation function wrapInArray(value: T | T[]): T[] { if (Array.isArray(value)) { return value; } else { return [value]; } } // Testing the solution console.log("=== Testing wrapInArray ===\n"); // Test 1: Single values console.log("Single values:"); console.log(wrapInArray(42)); // [42] console.log(wrapInArray("hello")); // ["hello"] console.log(wrapInArray(true)); // [true] // Test 2: Already arrays console.log("\nAlready arrays:"); console.log(wrapInArray([1, 2, 3])); // [1, 2, 3] console.log(wrapInArray(["a", "b"])); // ["a", "b"] // Test 3: Type preservation console.log("\nType preservation:"); const nums: number[] = wrapInArray(42); console.log(nums); // [42] const strs: string[] = wrapInArray(["x", "y"]); console.log(strs); // ["x", "y"] // Test 4: Objects console.log("\nObjects:"); const obj = { name: "Alice", age: 30 }; const wrapped = wrapInArray(obj); console.log(wrapped); // [{ name: "Alice", age: 30 }] ================================================================================ EXPECTED OUTPUT: ================================================================================ === Testing wrapInArray === Single values: [ 42 ] [ 'hello' ] [ true ] Already arrays: [ 1, 2, 3 ] [ 'a', 'b' ] Type preservation: [ 42 ] [ 'x', 'y' ] Objects: [ { name: 'Alice', age: 30 } ] ================================================================================ WHY THIS WORKS: ================================================================================ 1. Generic Overloads - is a type variable that represents any type - First overload: function wrapInArray(value: T): T[] → If you pass a T (single value), you get back T[] (array of T) - Second overload: function wrapInArray(value: T[]): T[] → If you pass a T[] (array), you get back T[] (same array) 2. Type Preservation When you call wrapInArray(42): - TypeScript infers T = number - The first overload matches: number → number[] - Return type is number[] When you call wrapInArray([1, 2, 3]): - TypeScript infers T = number - The second overload matches: number[] → number[] - Return type is number[] 3. Array.isArray() Check - At runtime, Array.isArray() checks if value is an array - If true, return it as-is - If false, wrap it in an array 4. Union Type in Implementation - function wrapInArray(value: T | T[]): T[] - Accepts either a single T or an array T[] - But returns T[] in both cases ================================================================================ ALTERNATIVE APPROACHES: ================================================================================ Approach 1: Using conditional types (advanced) type Wrapped = T extends any[] ? T : [T]; function wrapInArray(value: T): Wrapped { return (Array.isArray(value) ? value : [value]) as Wrapped; } // More powerful but more complex Approach 2: Without overloads (simpler but less typed) function wrapInArray(value: T | T[]): T[] { return Array.isArray(value) ? value : [value]; } // Works but TypeScript doesn't know as much about the types Approach 3: Separate function names function wrap(value: T): T[] { return [value]; } function ensure(value: T[]): T[] { return value; } // More explicit but requires different function names ================================================================================ HOW GENERICS WITH OVERLOADS WORK: ================================================================================ Step 1: Define overloads with generic constraints function wrapInArray(value: T): T[]; // Generic T function wrapInArray(value: T[]): T[]; // Same T, different parameter Step 2: Implementation signature function wrapInArray(value: T | T[]): T[] { // Both cases handled here } Step 3: TypeScript's matching algorithm - When you call wrapInArray(42): → Try first overload: 42 matches T (so T=number) → Success! Return type is T[] = number[] - When you call wrapInArray([1, 2]): → Try first overload: [1, 2] doesn't match T (it matches T[]) → Try second overload: [1, 2] matches T[] (so T=number) → Success! Return type is T[] = number[] ================================================================================ REAL-WORLD EXAMPLES: ================================================================================ Example 1: Promise.all (real TypeScript API) // Simplified version of how Promise.all works function all(value: T | Promise): Promise; function all(values: (T | Promise)[]): Promise; function all(value: any): Promise { if (Array.isArray(value)) { return Promise.all(value); } else { return Promise.resolve(value); } } // Usage const single = await all(42); // Promise<42> const multiple = await all([1, 2]); // Promise Example 2: Flatten single or nested structure function flatten(value: T | T[]): T[] { return Array.isArray(value) ? value : [value]; } // Real-world: normalizing API responses const user = { id: 1, name: "Alice" }; const users = [{ id: 1 }, { id: 2 }]; const normalized1 = flatten(user); // Always an array const normalized2 = flatten(users); // Already an array // Now you can process consistently normalized1.forEach(u => console.log(u.id)); normalized2.forEach(u => console.log(u.id)); Example 3: Query selector (could return single or multiple) function select(selector: string): T | T[] { const result = document.querySelectorAll(selector); if (result.length === 1) { return result[0] as T; } return Array.from(result) as T[]; } // Usage const button = select(".btn"); const buttons = select("button"); ================================================================================ TESTING & VERIFICATION: ================================================================================ // Test with different types console.log("=== Comprehensive Type Testing ===\n"); // Numbers const num = wrapInArray(42); console.log("Number:", num); console.log("Type check: is array?", Array.isArray(num)); console.log("First element:", num[0]); console.log(""); // Strings const str = wrapInArray("hello"); console.log("String:", str); console.log("Length:", str.length); console.log("First element:", str[0]); console.log(""); // Objects interface User { id: number; name: string; } const user: User = { id: 1, name: "Alice" }; const wrappedUser = wrapInArray(user); console.log("Object:", wrappedUser); console.log("First element name:", wrappedUser[0].name); console.log(""); // Arrays const numbers = wrapInArray([1, 2, 3]); console.log("Number array:", numbers); console.log("Is same array?", numbers === [1, 2, 3]); // Should be false (new array in wrap) console.log("Length:", numbers.length); console.log(""); // Empty array const empty = wrapInArray([]); console.log("Empty array:", empty); console.log("Length:", empty.length); console.log(""); // Nested test const nested = wrapInArray([wrapInArray(1), wrapInArray(2)]); console.log("Nested:", nested); console.log("Type: array of arrays?", Array.isArray(nested[0])); ================================================================================ EDGE CASES TO CONSIDER: ================================================================================ Edge Case 1: null and undefined function wrapInArray(value: T | T[]): T[] { return Array.isArray(value) ? value : [value]; } // These work fine wrapInArray(null); // [null] wrapInArray(undefined); // [undefined] // But you might want to handle them specially: function wrapInArraySafe(value: T | T[] | null | undefined): (T | null)[] { if (value === null || value === undefined) { return []; // Skip null/undefined } return Array.isArray(value) ? value : [value]; } Edge Case 2: Mixed nested arrays const mixed = [1, [2, 3]]; // number | number[] const wrapped = wrapInArray(mixed); // [number | number[]][] Edge Case 3: Tuples vs Arrays const tuple: [number, string] = [1, "hello"]; const wrapped = wrapInArray(tuple); // Treated as array, returns as-is ================================================================================ KEY TAKEAWAYS: ================================================================================ ✅ Generic overloads preserve type information through transformations ✅ in the signature means "any type" - TypeScript infers it ✅ Overloads match the most specific signature first ✅ Array.isArray() is the runtime check for arrays ✅ Return type T[] means "array of whatever T is" ✅ This pattern is used in real TypeScript APIs (Promise.all, etc.) Pattern Recognition: - Input: T or T[] → Output: T[] - This says "normalize to array, preserving element type" - Commonly used in libraries for flexible APIs ================================================================================