# ADR-014: Adopción de Nil-Safety Patterns con Optional Chaining y Nullish Coalescing **Estado:** ✅ Aceptado **Fecha:** 2025-11-23 **Autores:** Frontend-Developer, Architecture-Analyst **Decisión:** Adoptar Optional Chaining (`?.`) y Nullish Coalescing (`??`) como patrón estándar para manejo de valores null/undefined **Tags:** frontend, typescript, patterns, nil-safety, best-practices --- ## Contexto El frontend de GAMILIT maneja datos que pueden ser `null` o `undefined` en múltiples escenarios: - API responses durante loading states - Datos opcionales del usuario (perfil incompleto) - Datos anidados profundos (user.profile.gamification.rank) - Arrays que pueden estar vacíos o undefined ### Situación Inicial **Problema:** Sin un patrón estándar para manejar valores null/undefined **Variantes encontradas en el código:** ```typescript // Variante 1: Logical OR (||) - INCORRECTA const rank = user.gamification.rank || 'Sin rango'; // ❌ Falla si rank = 0 o rank = '' // Variante 2: Ternary con checks manuales - VERBOSE const rank = user && user.gamification && user.gamification.rank ? user.gamification.rank : 'Sin rango'; // ❌ Difícil de leer // Variante 3: Try-catch - ANTI-PATTERN try { const rank = user.gamification.rank; } catch { const rank = 'Sin rango'; } // ❌ Try-catch NO es para control de flujo // Variante 4: Lodash get - DEPENDENCIA INNECESARIA import { get } from 'lodash'; const rank = get(user, 'gamification.rank', 'Sin rango'); // ❌ Requiere lodash (+25 KB) ``` ### Problemas Identificados #### 1. TypeError: Cannot Read Property **Error más común en producción:** ``` TypeError: Cannot read property 'rank' of undefined at TeacherStudentsPage.tsx:42 ``` **Causa:** Acceso a propiedades sin validar null/undefined ```typescript // ❌ Código propenso a crash const rank = user.gamification.rank; // ^^^^^^^^^^^^ // Si gamification es undefined → CRASH ``` #### 2. Logical OR (||) Con Falsy Values **Bug real (BUG-FRONTEND-005):** ```typescript // ❌ Lógica incorrecta const count = comodinesInventory.pistas_count || 0; // Si pistas_count = 0 (tiene 0 pistas) // → count = 0 (CORRECTO por suerte) // PERO si queremos mostrar: const message = user.name || 'Anónimo'; // Si name = '' (string vacío válido) // → message = 'Anónimo' ❌ (debería ser '') ``` **Problema:** `||` considera TODOS los falsy values (0, '', false, null, undefined) #### 3. Nested Checks Verbosos ```typescript // ❌ Código repetitivo y difícil de leer const avatar = user && user.profile && user.profile.avatar ? user.profile.avatar : '/default-avatar.png'; // 4 líneas para un simple null check ``` --- ## Decisión **Adoptamos Optional Chaining (`?.`) + Nullish Coalescing (`??`)** como patrón estándar para nil-safety. ### Sintaxis **Optional Chaining (`?.`):** ```typescript // Acceso seguro a propiedades user?.profile?.avatar // Equivale a: user !== null && user !== undefined && user.profile !== null && user.profile !== undefined ? user.profile.avatar : undefined ``` **Nullish Coalescing (`??`):** ```typescript // Default value SOLO para null/undefined value ?? defaultValue // Equivale a: value !== null && value !== undefined ? value : defaultValue ``` ### Implementación Estándar **Patrón recomendado:** ```typescript // ✅ CORRECTO: Optional chaining + Nullish coalescing const rank = user?.gamification?.rank ?? 'Sin rango'; ``` **Por qué funciona:** 1. `user?.gamification?.rank` retorna el valor o `undefined` 2. `?? 'Sin rango'` aplica default SOLO si es `null` o `undefined` 3. Respeta falsy values válidos (0, '', false) --- ## Alternativas Consideradas ### Alternativa 1: Logical OR (`||`) **Descripción:** Usar operador OR lógico para default values **Pros:** - ✅ Sintaxis corta - ✅ Soportado en ES5+ (no requiere transpiling) **Cons:** - ❌ Falla con falsy values válidos (0, '', false) - ❌ Comportamiento inesperado para principiantes - ❌ No es semánticamente correcto (|| es para lógica booleana) **Ejemplo:** ```typescript const count = inventory.pistas_count || 0; // ❌ Si count = 0 → retorna 0 (correcto por casualidad) // ❌ Si name = '' → retorna 'Anónimo' (INCORRECTO) ``` **Veredicto:** ❌ **RECHAZADA** - Comportamiento inconsistente con falsy values --- ### Alternativa 2: Default Parameters **Descripción:** Usar default parameters en funciones **Pros:** - ✅ Type-safe - ✅ Semánticamente correcto - ✅ Solo aplica para undefined (no null) **Cons:** - ⚠️ Solo funciona en function parameters (no object properties) - ❌ No sirve para nested access **Ejemplo:** ```typescript function greet(name: string = 'Anónimo') { return `Hola, ${name}`; } // ✅ Funciona para parameters greet(); // 'Hola, Anónimo' greet(undefined); // 'Hola, Anónimo' // ❌ NO funciona para object access const rank = user.gamification?.rank = 'Sin rango'; // Syntax error ``` **Veredicto:** ⚠️ **USO LIMITADO** - Solo para function parameters, no reemplaza nil-safety --- ### Alternativa 3: Lodash `get()` **Descripción:** Usar `_.get()` de Lodash para safe property access **Pros:** - ✅ Safe deep property access - ✅ Default value support - ✅ Battle-tested (usado por millones) **Cons:** - ❌ Requiere dependencia externa (+25 KB con tree-shaking, +70 KB sin) - ❌ Performance overhead (función adicional) - ❌ String-based paths (no type-safe) - ❌ Menos legible que native syntax **Ejemplo:** ```typescript import { get } from 'lodash'; const rank = get(user, 'gamification.rank', 'Sin rango'); // ^^^^^^^^^^^^^^^^^^ // ❌ String path: no autocomplete, no type checking ``` **Veredicto:** ❌ **RECHAZADA** - Bundle size injustificado cuando TypeScript tiene sintaxis nativa --- ### Alternativa 4: Optional Chaining + Nullish Coalescing (TypeScript Native) **Descripción:** Usar sintaxis nativa de TypeScript/ES2020 **Pros:** - ✅ Sintaxis nativa (0 KB bundle overhead) - ✅ Type-safe (TypeScript valida en compile-time) - ✅ Legible y conciso - ✅ Performance óptimo (compila a checks simples) - ✅ Estándar ECMAScript (ES2020) - ✅ Soportado por todos los browsers modernos **Cons:** - ⚠️ Requiere TypeScript 3.7+ (ya lo tenemos) - ⚠️ Requires transpiling para IE11 (no es concern - no soportamos IE11) **Ejemplo:** ```typescript const rank = user?.gamification?.rank ?? 'Sin rango'; // ✅ Type-safe // ✅ Autocomplete // ✅ Compila a código eficiente ``` **Veredicto:** ✅ **SELECCIONADA** - Mejor opción en todos los aspectos --- ## Tabla Comparativa | Característica | `||` | Default Params | Lodash `get()` | `?.` + `??` | |----------------|------|----------------|----------------|-------------| | **Bundle Size** | 0 KB | 0 KB | 25 KB | 0 KB | | **Type-Safe** | ❌ No | ✅ Sí | ❌ No | ✅ Sí | | **Falsy Values** | ❌ Falla | ✅ OK | ✅ OK | ✅ OK | | **Deep Nesting** | ⚠️ Verbose | ❌ No | ✅ Sí | ✅ Sí | | **Legibilidad** | ⚠️ Media | ✅ Alta | ⚠️ Media | ✅ Alta | | **Performance** | ✅ Alta | ✅ Alta | ⚠️ Media | ✅ Alta | | **Standard** | ES5 | ES6 | N/A | ES2020 | **Conclusión:** Optional Chaining + Nullish Coalescing es la mejor opción. --- ## Consecuencias ### Positivas ✅ #### 1. Código Más Conciso **Antes:** ```typescript const rank = user && user.gamification && user.gamification.rank ? user.gamification.rank : 'Sin rango'; ``` **Después:** ```typescript const rank = user?.gamification?.rank ?? 'Sin rango'; ``` **Reducción:** 4 líneas → 1 línea (75% menos código) #### 2. Type-Safety Garantizado ```typescript // ✅ TypeScript valida tipos interface User { gamification?: { rank?: string; }; } const rank = user?.gamification?.rank ?? 'Sin rango'; // ^^^^ // TypeScript sabe que rank es string | undefined ``` #### 3. Prevención de TypeError **Antes (propenso a crashes):** ```typescript const rank = user.gamification.rank; // ❌ Crash si gamification undefined ``` **Después (safe):** ```typescript const rank = user?.gamification?.rank ?? 'Sin rango'; // ✅ Nunca crashea ``` #### 4. Zero Bundle Overhead **Comparación:** - Lodash `get()`: +25 KB - Optional Chaining + Nullish Coalescing: **0 KB** (sintaxis nativa) **Transpiled output (TypeScript → JavaScript):** ```javascript // Input TypeScript const rank = user?.gamification?.rank ?? 'Sin rango'; // Output JavaScript (ES2020 target) const rank = user?.gamification?.rank ?? 'Sin rango'; // Output JavaScript (ES5 target - legacy) var _a, _b; const rank = (_b = (_a = user) === null || _a === void 0 ? void 0 : _a.gamification) === null || _b === void 0 ? void 0 : _b.rank) !== null && _b !== void 0 ? _b : 'Sin rango'; ``` **Bundle impact:** Mínimo (<1% overhead en ES5 target) --- ### Negativas ⚠️ #### 1. Curva de Aprendizaje Inicial **Conceptos nuevos:** - Diferencia entre `?.` y `.` - Diferencia entre `??` y `||` - Cuándo usar cada operador **Mitigación:** - ✅ Documentación interna con ejemplos - ✅ ESLint rule para detectar uso de `||` donde debería ser `??` - ✅ Code reviews #### 2. Puede Ocultar Bugs de Datos Faltantes **Ejemplo:** ```typescript // ❌ Anti-pattern: ocultar bugs const rank = user?.gamification?.rank ?? 'Sin rango'; // Si gamification DEBERÍA existir siempre, este code oculta el bug // Mejor validar explícitamente: if (!user?.gamification) { console.error('User missing gamification data'); Sentry.captureException(new Error('Missing gamification')); } const rank = user.gamification.rank ?? 'Sin rango'; ``` **Guía:** - ✅ Usar `?.` cuando datos son OPCIONALMENTE opcionales - ❌ NO usar `?.` para "esconder" datos que DEBERÍAN existir --- ## Guía de Uso ### Caso 1: Nested Property Access ```typescript // ✅ CORRECTO const avatar = user?.profile?.avatar ?? '/default-avatar.png'; // ❌ INCORRECTO const avatar = (user && user.profile && user.profile.avatar) || '/default-avatar.png'; ``` ### Caso 2: Array Length ```typescript // ✅ CORRECTO const count = students?.length ?? 0; // ❌ INCORRECTO const count = students ? students.length : 0; ``` ### Caso 3: Falsy Values Válidos ```typescript // ✅ CORRECTO (respeta 0) const pistas = inventory?.pistas_count ?? 0; // Si pistas_count = 0 → retorna 0 ✅ // ❌ INCORRECTO (falla con 0) const pistas = inventory?.pistas_count || 0; // Si pistas_count = 0 → retorna 0 (correcto por suerte, pero mal pattern) ``` ### Caso 4: Optional Function Calls ```typescript // ✅ CORRECTO const result = obj?.method?.(); // Equivale a: const result = obj && obj.method ? obj.method() : undefined; ``` --- ## Anti-Patterns a Evitar ### Anti-Pattern 1: Usar `||` Para Default Values ```typescript // ❌ MALO const name = user.name || 'Anónimo'; // Falla si name = '' // ✅ BUENO const name = user.name ?? 'Anónimo'; // Respeta '' ``` ### Anti-Pattern 2: Over-using Optional Chaining ```typescript // ❌ MALO (oculta bugs) const rank = user?.gamification?.rank ?? 'Sin rango'; // Si gamification SIEMPRE debería existir, esto esconde el bug // ✅ BUENO (valida explícitamente) if (!user.gamification) { throw new Error('User missing gamification data'); } const rank = user.gamification.rank ?? 'Sin rango'; ``` ### Anti-Pattern 3: Mixing `||` and `??` ```typescript // ❌ CONFUSO const value = data?.field || defaultValue ?? fallback; // ✅ CLARO const value = data?.field ?? defaultValue; ``` --- ## Ejemplos Reales del Proyecto ### Ejemplo 1: TeacherStudentsPage ```typescript // apps/frontend/src/apps/teacher/pages/TeacherStudents.tsx // ✅ Safe rank display const rank = student.user?.gamification?.rank ?? 'Sin rango'; // ✅ Safe student count const studentCount = students?.length ?? 0; // ✅ Safe classroom name const classroomName = selectedClassroom?.name ?? 'Todos los estudiantes'; ``` ### Ejemplo 2: GamificationWidget ```typescript // ✅ Safe XP display const xp = gamificationData?.xp ?? 0; // ✅ Safe coins display const coins = gamificationData?.coins ?? 0; // ✅ Safe achievements count const achievementsCount = gamificationData?.achievements?.length ?? 0; ``` --- ## TypeScript Configuration **Requerido en tsconfig.json:** ```json { "compilerOptions": { "strict": true, "strictNullChecks": true, // ✅ REQUERIDO "target": "ES2020", // ✅ Para native support "lib": ["ES2020", "DOM"] } } ``` **Con `strictNullChecks: true`:** - TypeScript diferencia `T` vs `T | null` vs `T | undefined` - Fuerza uso explícito de null checks - Previene accesos inseguros en compile-time --- ## ESLint Rules (Recomendadas) ```javascript // .eslintrc.js module.exports = { rules: { // Warn cuando se usa || con objetos (probablemente debería ser ??) 'prefer-nullish-coalescing': 'warn', // Warn cuando se usa && para property access (debería ser ?.) 'prefer-optional-chain': 'warn', }, }; ``` --- ## Referencias - [TC39 Optional Chaining Proposal](https://github.com/tc39/proposal-optional-chaining) - [TC39 Nullish Coalescing Proposal](https://github.com/tc39/proposal-nullish-coalescing) - [TypeScript 3.7 Release Notes](https://www.typescriptlang.org/docs/handbook/release-notes/typescript-3-7.html) - [MDN: Optional Chaining](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/Optional_chaining) - [MDN: Nullish Coalescing](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/Nullish_coalescing) - [Implementation in TeacherStudentsPage](../apps/frontend/src/apps/teacher/pages/TeacherStudents.tsx) --- **Versión:** 1.0.0 **Última actualización:** 2025-11-24 **Estado:** ✅ Aceptado e Implementado **Proyecto:** GAMILIT - Sistema de Gamificación Educativa