TypeScript Anti-Pattern: Optional Chaining in Loops

A common TypeScript anti-pattern I keep seeing. When working with nested arrays, many developers lean on optional chaining everywhere, especially inside loops. It feels safe. It feels concise. But it quietly introduces unnecessary branching and noisy logic. --- 🛡️ The “defensive everywhere” approach ``` function getActiveEmails(data?: ApiResponse) { return data?.users ?.filter(u => u?.isActive) ?.map(u => u?.profile?.email ?? "no-email") ?? []; } ``` What’s happening here? - "data?.users" → branch - "u?.isActive" → branch per iteration - "u?.profile?.email" → multiple branches per iteration - "?? []" → fallback branch You’ve now created a combinatorial explosion of paths inside what should be a simple transformation. Your tests? They now need to cover: - missing "data" - missing "users" - partially invalid users - missing profiles - missing emails All implicitly baked into one chain. --- ✅ Early return & controlled assumptions ``` function getActiveEmails(data?: ApiResponse) { if (!data?.users) return []; return data.users .filter(user => user.isActive) .map(user => user.profile.email); } ``` Key differences: - The guard ("if") still uses optional chaining, but only once, at the boundary - Inside the loop, we treat data as valid by contract - No defensive noise inside "filter"/"map" - The “happy path” is clean and linear --- Why? Optional chaining inside iteration is especially costly: - It introduces branches per element - It hides data contract issues - It bloats test cases unnecessarily - It makes bugs harder to spot (everything just becomes “undefined”) --- So, - Use optional chaining at the edges of your system - Use early returns to establish guarantees - Keep your core logic branch light and explicit --- Optional chaining isn’t the problem. Unstructured defensiveness is. #TypeScript #JavaScript #CleanCode #CodeQuality #Refactoring #BestPractices #DeveloperTips

To view or add a comment, sign in

Explore content categories