workspace/projects/gamilit/docs/97-adr/ADR-014-nil-safety-patterns.md
rckrdmrd ea1879f4ad feat: Initial workspace structure with multi-level Git configuration
- 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>
2025-12-08 10:44:23 -06:00

555 lines
14 KiB
Markdown

# 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