- Configure workspace Git repository with comprehensive .gitignore - Add Odoo as submodule for ERP reference code - Include documentation: SETUP.md, GIT-STRUCTURE.md - Add gitignore templates for projects (backend, frontend, database) - Structure supports independent repos per project/subproject level Workspace includes: - core/ - Reusable patterns, modules, orchestration system - projects/ - Active projects (erp-suite, gamilit, trading-platform, etc.) - knowledge-base/ - Reference code and patterns (includes Odoo submodule) - devtools/ - Development tools and templates - customers/ - Client implementations template 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
14 KiB
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:
// 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
// ❌ 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):
// ❌ 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
// ❌ 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 (?.):
// 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 (??):
// Default value SOLO para null/undefined
value ?? defaultValue
// Equivale a:
value !== null && value !== undefined
? value
: defaultValue
Implementación Estándar
Patrón recomendado:
// ✅ CORRECTO: Optional chaining + Nullish coalescing
const rank = user?.gamification?.rank ?? 'Sin rango';
Por qué funciona:
user?.gamification?.rankretorna el valor oundefined?? 'Sin rango'aplica default SOLO si esnulloundefined- 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:
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:
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:
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:
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:
const rank = user && user.gamification && user.gamification.rank
? user.gamification.rank
: 'Sin rango';
Después:
const rank = user?.gamification?.rank ?? 'Sin rango';
Reducción: 4 líneas → 1 línea (75% menos código)
2. Type-Safety Garantizado
// ✅ 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):
const rank = user.gamification.rank; // ❌ Crash si gamification undefined
Después (safe):
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):
// 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:
// ❌ 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
// ✅ 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
// ✅ CORRECTO
const count = students?.length ?? 0;
// ❌ INCORRECTO
const count = students ? students.length : 0;
Caso 3: Falsy Values Válidos
// ✅ 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
// ✅ 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
// ❌ 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
// ❌ 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 ??
// ❌ CONFUSO
const value = data?.field || defaultValue ?? fallback;
// ✅ CLARO
const value = data?.field ?? defaultValue;
Ejemplos Reales del Proyecto
Ejemplo 1: TeacherStudentsPage
// 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
// ✅ 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:
{
"compilerOptions": {
"strict": true,
"strictNullChecks": true, // ✅ REQUERIDO
"target": "ES2020", // ✅ Para native support
"lib": ["ES2020", "DOM"]
}
}
Con strictNullChecks: true:
- TypeScript diferencia
TvsT | nullvsT | undefined - Fuerza uso explícito de null checks
- Previene accesos inseguros en compile-time
ESLint Rules (Recomendadas)
// .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
- TC39 Nullish Coalescing Proposal
- TypeScript 3.7 Release Notes
- MDN: Optional Chaining
- MDN: Nullish Coalescing
- Implementation in TeacherStudentsPage
Versión: 1.0.0 Última actualización: 2025-11-24 Estado: ✅ Aceptado e Implementado Proyecto: GAMILIT - Sistema de Gamificación Educativa