================================================================================ TYPESCRIPT FUNDAMENTALS - CHALLENGE 2 SOLUTION Chapter 9: Enums & Literal Types Challenge: Literal Type Union ================================================================================ PROBLEM: Create a LogLevel type using literal union that represents log levels: "debug", "info", "warn", "error". Write a logger function that outputs with appropriate formatting. SOLUTION: ================================================================================ // Define LogLevel as literal union type type LogLevel = "debug" | "info" | "warn" | "error"; // Logger function with different formatting per level function log(level: LogLevel, message: string): void { const timestamp = new Date().toISOString(); switch (level) { case "debug": console.log(`[DEBUG] ${timestamp} - ${message}`); break; case "info": console.log(`[INFO] ${timestamp} - ${message}`); break; case "warn": console.warn(`[WARN] ${timestamp} - ${message}`); break; case "error": console.error(`[ERROR] ${timestamp} - ${message}`); break; } } // Convenience functions function debug(message: string) { log("debug", message); } function info(message: string) { log("info", message); } function warn(message: string) { log("warn", message); } function error(message: string) { log("error", message); } // Testing the solution console.log("=== Logger Test ===\n"); debug("Application started"); info("User logged in successfully"); warn("Deprecation notice: use newMethod() instead"); error("Failed to fetch data from API"); // Type-safe: won't allow invalid levels // log("verbose", "message"); // ❌ Error! "verbose" is not a LogLevel ================================================================================ EXPECTED OUTPUT: ================================================================================ === Logger Test === [DEBUG] 2026-06-29T12:34:56.789Z - Application started [INFO] 2026-06-29T12:34:56.790Z - User logged in successfully [WARN] 2026-06-29T12:34:56.791Z - Deprecation notice: use newMethod() instead [ERROR] 2026-06-29T12:34:56.792Z - Failed to fetch data from API ================================================================================ WHY THIS WORKS: ================================================================================ 1. Literal Union Type type LogLevel = "debug" | "info" | "warn" | "error"; - This defines exactly 4 allowed values - No other strings are accepted - TypeScript enforces this at compile-time 2. Exhaustiveness Checking In the switch statement: - case "debug": ... - case "info": ... - case "warn": ... - case "error": ... If you add a new log level to the union type but forget to add a case, TypeScript will error because the switch isn't exhaustive. 3. Convenience Functions - debug(), info(), warn(), error() don't need the level parameter - They call log() with the appropriate level - More convenient for callers who know which level they want 4. Type Safety Benefits - log("debug", msg); ✅ OK - log("DEBUG", msg); ❌ Error (wrong case) - log("verbose", msg); ❌ Error (not in union) - debug(msg); ✅ OK (convenience function) ================================================================================ ALTERNATIVE APPROACHES: ================================================================================ Approach 1: Using Enum (More Traditional) enum LogLevel { Debug = "debug", Info = "info", Warn = "warn", Error = "error" } function log(level: LogLevel, message: string): void { // ... same implementation } // Usage: log(LogLevel.Debug, message); Approach 2: Object-based Logger (Modern) const logger = { debug(message: string) { console.log(`[DEBUG] ${message}`); }, info(message: string) { console.log(`[INFO] ${message}`); }, warn(message: string) { console.warn(`[WARN] ${message}`); }, error(message: string) { console.error(`[ERROR] ${message}`); } }; // Usage: logger.debug("msg"); Approach 3: Class-based Logger class Logger { log(level: LogLevel, message: string) { /* ... */ } debug(message: string) { this.log("debug", message); } info(message: string) { this.log("info", message); } warn(message: string) { this.log("warn", message); } error(message: string) { this.log("error", message); } } const logger = new Logger(); logger.info("User logged in"); ================================================================================ ADVANCED: LOGGER WITH FILTERING ================================================================================ // Logger that only outputs levels at or above a threshold type LogLevel = "debug" | "info" | "warn" | "error"; const LogLevelPriority: Record = { debug: 0, info: 1, warn: 2, error: 3 }; class FilteredLogger { constructor(private minLevel: LogLevel) {} log(level: LogLevel, message: string): void { if (LogLevelPriority[level] >= LogLevelPriority[this.minLevel]) { const timestamp = new Date().toISOString(); console.log(`[${level.toUpperCase()}] ${timestamp} - ${message}`); } } debug(message: string) { this.log("debug", message); } info(message: string) { this.log("info", message); } warn(message: string) { this.log("warn", message); } error(message: string) { this.log("error", message); } } // Only show warnings and errors const prodLogger = new FilteredLogger("warn"); prodLogger.debug("This won't show"); // Filtered out prodLogger.warn("Database slow"); // Shows prodLogger.error("Connection failed"); // Shows // Show everything in development const devLogger = new FilteredLogger("debug"); devLogger.debug("Variable x = 42"); // Shows devLogger.error("Error occurred"); // Shows ================================================================================ REAL-WORLD EXAMPLES: ================================================================================ Example 1: Winston-like Logger Interface type LogLevel = "debug" | "info" | "warn" | "error"; interface Logger { log(level: LogLevel, message: string): void; debug(message: string): void; info(message: string): void; warn(message: string): void; error(message: string): void; } Example 2: Syslog Levels (UNIX standard) type SyslogLevel = | "emergency" // 0: System unusable | "alert" // 1: Action must be taken immediately | "critical" // 2: Critical condition | "error" // 3: Error condition | "warning" // 4: Warning condition | "notice" // 5: Normal but significant condition | "info" // 6: Informational message | "debug"; // 7: Debug-level message Example 3: Severity with Multiple Attributes type LogEntry = { level: "debug" | "info" | "warn" | "error"; message: string; timestamp: Date; source: string; }; function processLog(entry: LogEntry) { const color = entry.level === "error" ? "red" : "black"; console.log(`%c[${entry.level}]`, `color: ${color}`); console.log(`${entry.timestamp.toISOString()} | ${entry.source} | ${entry.message}`); } ================================================================================ TESTING & VERIFICATION: ================================================================================ console.log("=== Logger Type Safety Test ===\n"); // Test 1: Valid log levels console.log("Test 1 - Valid levels:"); const validLevels: LogLevel[] = ["debug", "info", "warn", "error"]; validLevels.forEach(level => { log(level, `This is a ${level} message`); }); // Test 2: Convenience functions console.log("\nTest 2 - Convenience functions:"); debug("Debug message via convenience function"); info("Info message via convenience function"); warn("Warn message via convenience function"); error("Error message via convenience function"); // Test 3: Invalid level (compile error) console.log("\nTest 3 - Type safety:"); console.log("These would cause compile errors:"); console.log(' log("verbose", "msg"); ❌ "verbose" is not a LogLevel'); console.log(' log("DEBUG", "msg"); ❌ Case mismatch'); // Test 4: Using in a filtering context console.log("\nTest 4 - Switch exhaustiveness:"); function getColorForLevel(level: LogLevel): string { switch (level) { case "debug": return "gray"; case "info": return "blue"; case "warn": return "orange"; case "error": return "red"; // No default needed - all cases covered! } } console.log("Color mapping:"); console.log(` debug -> ${getColorForLevel("debug")}`); console.log(` info -> ${getColorForLevel("info")}`); console.log(` warn -> ${getColorForLevel("warn")}`); console.log(` error -> ${getColorForLevel("error")}`); // Test 5: Type as array console.log("\nTest 5 - Iterating over literal union:"); const allLevels: LogLevel[] = ["debug", "info", "warn", "error"]; allLevels.forEach(level => { console.log(`Level: ${level}`); }); ================================================================================ KEY TAKEAWAYS: ================================================================================ ✅ Literal unions are simpler than enums for string constants ✅ TypeScript enforces the exact allowed values ✅ Switch statements with literal unions enable exhaustiveness checking ✅ Convenience functions make the API easier to use ✅ Type-safe: invalid values cause compile errors, not runtime bugs ✅ Literal unions work great with discriminated unions Literal Union Benefits: - No runtime object created (unlike enums) - Simpler syntax - Better type inference - Works seamlessly with union types - Modern TypeScript best practice When to Use Literal Unions: ✅ Fixed set of string values (status, level, type) ✅ Combined with discriminated unions ✅ Simple constant values ✅ Modern, clean code When to Use Enums: ✅ Need reverse mapping (numeric) ✅ Legacy codebase already using enums ✅ Very large sets of values ✅ Need enum-specific features ================================================================================