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

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:

  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:

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 T vs T | null vs T | 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


Versión: 1.0.0 Última actualización: 2025-11-24 Estado: Aceptado e Implementado Proyecto: GAMILIT - Sistema de Gamificación Educativa