Operadores modernos de JavaScript: ??=, ||=, &&=, ?., ?? y patrones de uso

ES2020 y ES2021 introdujeron un conjunto de operadores que simplifican patrones muy frecuentes en JavaScript: el manejo de valores nulos o indefinidos y la asignación condicional. El nullish coalescing (??), optional chaining (?.) y los operadores de asignación lógica (??=, ||=, &&=) son hoy herramientas cotidianas en cualquier código moderno.

Nullish coalescing (??): el problema de ||

El operador || devuelve el segundo operando cuando el primero es falsy (false, 0, '', null, undefined, NaN). Esto causa bugs cuando 0 o '' son valores válidos. El operador ?? solo considera null y undefined como "sin valor":

// El bug clásico con ||
function configurarTimeout(opciones) {
  // Si opciones.timeout es 0 (desactivado), || lo trata como falsy
  const timeout = opciones.timeout || 5000;
  // timeout === 5000 incluso si se pasó 0 explícitamente ? BUG
}

// Con ??: 0 es un valor válido
function configurarTimeoutBien(opciones) {
  const timeout = opciones.timeout ?? 5000;
  // Si opciones.timeout === 0, timeout === 0 ?
  // Si opciones.timeout === undefined o null, timeout === 5000 ?
}

// Ejemplos prácticos
const config = { debug: false, pagina: 0, nombre: '' };

config.debug || true;     // true — ¡bug! false es falsy
config.debug ?? true;     // false — correcto

config.pagina || 1;       // 1 — ¡bug! 0 es falsy
config.pagina ?? 1;       // 0 — correcto

config.nombre || 'anon';  // 'anon' — ¡bug! '' es falsy
config.nombre ?? 'anon';  // '' — correcto

Optional chaining (?.) : acceso seguro a propiedades

?. evalúa la expresión de la izquierda; si es null o undefined, devuelve undefined en lugar de lanzar un TypeError. Encadena sin problemas con otros ?.:

const usuario = {
  nombre: 'Ana',
  // address no existe
};

// Sin optional chaining — múltiples comprobaciones manuales
const ciudad = usuario && usuario.address && usuario.address.city;

// Con optional chaining — limpio y seguro
const ciudadSegura = usuario?.address?.city;
console.log(ciudadSegura); // undefined — sin TypeError

// También funciona con métodos y notación de corchetes
const longitud = usuario?.nombre?.toUpperCase()?.length;
const primerPermiso = usuario?.permisos?.[0];
const resultado = usuario?.calcular?.('arg1', 'arg2');

// Combinado con ?? para valores por defecto
const ciudad2 = usuario?.address?.city ?? 'Sin ciudad';
console.log(ciudad2); // 'Sin ciudad'

Caso real: normalizar datos de API

// Respuesta de API con campos opcionales variables
function normalizarUsuario(raw) {
  return {
    id: raw.id,
    nombre: raw.name ?? raw.username ?? 'Anónimo',
    email: raw.email ?? null,
    avatar: raw.avatar_url ?? raw.profile?.picture?.url ?? '/img/default.png',
    ciudad: raw.location?.city ?? raw.address?.city ?? '',
    activo: raw.is_active ?? raw.active ?? true,
    rol: raw.role?.name ?? raw.permissions?.[0] ?? 'usuario',
  };
}

Operadores de asignación lógica

Los tres operadores de asignación lógica (ES2021) combinan la lógica de ??, || y && con la asignación, pero con una particularidad importante: solo asignan si la condición del operador se cumple. Si la condición no se cumple, no hay asignación (y no se disparan setters).

// ??= : asigna solo si el valor es null o undefined
let config = { timeout: null, debug: undefined, host: 'localhost' };

config.timeout ??= 5000;   // null ? asigna 5000
config.debug ??= false;    // undefined ? asigna false
config.host ??= 'default'; // 'localhost' ? NO asigna (ya tiene valor)

console.log(config); // { timeout: 5000, debug: false, host: 'localhost' }

// ||= : asigna solo si el valor actual es falsy
let nombre = '';
nombre ||= 'Anónimo'; // '' es falsy ? asigna
console.log(nombre);  // 'Anónimo'

let activo = false;
activo ||= true;      // false es falsy ? asigna
console.log(activo);  // true

// &&= : asigna solo si el valor actual es truthy
let usuario = { nombre: 'Ana' };
usuario.nombre &&= usuario.nombre.toUpperCase(); // truthy ? asigna
console.log(usuario.nombre); // 'ANA'

let sinNombre = null;
sinNombre &&= sinNombre.toUpperCase(); // null ? NO asigna, NO lanza error
console.log(sinNombre); // null

Diferencia crítica: asignación vs evaluación

// ??= NO es igual a: obj.prop = obj.prop ?? valor

let contador = 0;
let llamadas = 0;

const objeto = {
  get valor() {
    llamadas++;
    return this._valor;
  },
  set valor(v) {
    this._valor = v;
  },
  _valor: undefined,
};

// Con ??=: el setter NO se llama si el valor ya existe
objeto._valor = 'existente';
objeto.valor ??= 'nuevo'; // getter devuelve 'existente', no es null/undefined ? setter NO se llama

// Con asignación manual: el setter SIEMPRE se llama
objeto.valor = objeto.valor ?? 'nuevo'; // Llama al setter aunque no cambie nada

Patrones comunes en configuración fetch

// Construir opciones de fetch con defaults
function crearOpcionesFetch(opciones = {}) {
  opciones.method ??= 'GET';
  opciones.headers ??= {};
  opciones.headers['Content-Type'] ??= 'application/json';
  opciones.credentials ??= 'same-origin';
  opciones.timeout ??= 10_000;
  return opciones;
}

// Gestión de estado con ??=
class Store {
  #estado = {};

  setDefault(clave, valorDefecto) {
    this.#estado[clave] ??= valorDefecto;
  }

  activarSiExiste(clave) {
    this.#estado[clave] &&= { ...this.#estado[clave], activo: true };
  }
}

La distinción entre ?? y || es fundamental: si el código necesita tratar 0, false o '' como valores válidos, siempre hay que usar ??. Y los operadores de asignación lógica no son solo atajo de teclado; su semántica de no-asignación-si-innecesaria es relevante cuando hay getters y setters con lógica de negocio.

COMPARTE ESTE ARTÍCULO

COMPARTIR EN FACEBOOK
COMPARTIR EN TWITTER
COMPARTIR EN LINKEDIN
COMPARTIR EN WHATSAPP