Fundamentos JavaScript
Curso·De JavaScript a TypeScript estrictoRedacción asistida por IA

Operadores de ausencia: ?., ?? y ??=

Lección 5 de 43 · El lenguaje: de JavaScript a TypeScript estricto

Cesar Enrique Manzano Velasco
Cesar Enrique Manzano Velasco
29 de agosto de 2026
Operadores de ausencia: ?., ?? y ??=
De JavaScript a TypeScript estrictoLección 5/4

Temario del curso

De JavaScript a TypeScript estricto

Lección 5 de 4

CursoDe JavaScript a TypeScript estricto · Lección 5 de 4

La ausencia de un valor se representa de dos formas en JavaScript, null y undefined, y durante años el lenguaje no ofreció ninguna herramienta específica para tratarla. Se usaba || para los valores por defecto y cadenas de && para acceder a datos anidados, con los bugs que eso arrastraba.

Los tres operadores de esta lección existen para cubrir ese hueco. Entenderlos bien elimina una familia entera de errores que, además, son de los que solo aparecen con datos reales.

Nota sobre la terminología: este artículo usa los términos ingleses falsy y nullish porque son los que aparecen en la documentación y en los mensajes de error. Ambos se definen a continuación.

Nullish frente a falsy

Definición — Falsy: Conjunto de valores que se evalúan como falso en un contexto booleano. Son estos siete:

false    0    -0    0n    ""    null    undefined    NaN

Definición — Nullish: Conjunto mucho más pequeño: solo null y undefined.

null    undefined

La distinción parece pedante hasta que la aplicas a datos reales. 0 y "" son valores perfectamente legítimos: cero reintentos es una configuración válida, una nota vacía es una nota que alguien escribió vacía, un total de 0 es un total. En cambio, null y undefined significan otra cosa: no hay valor.

De ahí sale la regla que ordena toda la lección:

OperadoresActúan sobre??, ?., ??=Nullish||, &&, ||=Falsy

Ahí está toda la diferencia, y de ahí sale el bug de la sección siguiente.


El bug clásico de || con 0 y cadena vacía

Antes de que existiera ??, la forma habitual de dar un valor por defecto era ||:

ts

const reintentos = config.reintentos || 3;

Se lee como "usa config.reintentos, y si no hay nada, usa 3". Pero no es lo que hace. || devuelve el operando derecho cuando el izquierdo es falsy, no cuando falta. Si la configuración dice explícitamente cero reintentos, ese 0 es falsy y queda reemplazado por 3.

El sistema hace justo lo contrario de lo que se configuró, y no hay ningún error que lo delate.

Definición — Nullish coalescing (??): Operador que devuelve el operando derecho solo si el izquierdo es null o undefined.

ts

const reintentos = config.reintentos ?? 3; // 0 se respeta const malo = config.reintentos || 3; // 0 se convierte en 3

Comparados sobre los mismos valores:

Valor de entradaCon ||Con ??555030''defecto''null33undefined33

Los casos donde esto muerde de verdad son los que llevan cifras y textos que pueden estar vacíos de forma legítima: un descuento de 0, un contador en 0, un campo de texto que el usuario dejó en blanco a propósito, un false que significa "desactivado" y no "sin configurar".

?? no reemplaza a || en todos los casos. Cuando lo que quieres es precisamente descartar cualquier valor vacío, por ejemplo sustituir una cadena vacía por un texto de relleno, || es el operador correcto. Elige según la pregunta: ¿quiero descartar los vacíos, o solo lo ausente?

Un detalle de sintaxis: el lenguaje prohíbe mezclar ?? con || o && sin paréntesis, porque el resultado sería ambiguo al leerlo. Hay que agrupar de forma explícita:

ts

const valor = (a ?? b) || c; // los paréntesis son obligatorios

Encadenamiento opcional: ?.

Definición — Optional chaining (?.): Operador de acceso que detiene la expresión y devuelve undefined si lo que hay a su izquierda es null o undefined, en vez de lanzar un error.

El problema que resuelve

El acceso a datos anidados falla en cuanto un nivel intermedio no existe:

ts

const email = usuario.contacto.email; // si `contacto` es undefined, esto lanza

La solución antigua era una cadena de comprobaciones que crecía con cada nivel:

ts

const email = usuario && usuario.contacto && usuario.contacto.email;

El encadenamiento opcional expresa lo mismo en una línea legible:

ts

const email = usuario.contacto?.email;

Solo protege el eslabón que tiene a su izquierda

Esto es lo que hay que interiorizar: ?. no es un acceso seguro genérico.

ts

a.b?.c // si `a` es undefined, esto sigue lanzando: nada protege el acceso a `.b`

Las tres posiciones donde funciona

ts

usuario.contacto?.email; // propiedad usuario.obtenerNombre?.(); // llamada: solo llama si la función existe lineas?.[0]; // índice

La segunda forma es especialmente útil con callbacks opcionales: onFinish?.() llama al callback si se pasó y no hace nada si no. La tercera cubre arrays y accesos por clave dinámica.

Su consecuencia en el tipo

El resultado de una expresión con ?. siempre incluye undefined:

ts

const email = usuario.contacto?.email; // string | undefined

Eso está bien, porque refleja la realidad, pero significa que ?. no resuelve el problema: lo traslada un paso. El undefined sigue ahí y hay que decidir qué hacer con él.

Por eso ?. y ?? casi siempre aparecen juntos:

ts

const email = usuario.contacto?.email ?? 'sin-email'; // string

No abuses. Poner ?. en cada punto de una cadena "por si acaso" convierte errores ruidosos en undefined silenciosos que aparecen tres capas más abajo, mucho más lejos de la causa. Si un nivel intermedio no debería faltar, es mejor que falle ahí y se vea. ?. es para lo que es opcional de verdad, no para el miedo.


??=: asignar un valor por defecto una sola vez

Definición — Asignación nullish (??=): Asigna el valor de la derecha solo si la variable es null o undefined.

ts

process.env.DB_PORT ??= '5432';

Es equivalente a esto, pero sin repetir el nombre de la variable:

ts

if (process.env.DB_PORT === null || process.env.DB_PORT === undefined) { process.env.DB_PORT = '5432'; }

Definición — Idempotente: Que ejecutar la operación varias veces produce el mismo resultado que ejecutarla una sola vez.

Esa es la propiedad que hace útil a ??=. La primera vez asigna; a partir de ahí, como ya hay valor, no hace nada. Eso lo vuelve seguro en código que puede evaluarse más de una vez, como el arranque de un módulo que se reutiliza entre invocaciones.

Inicialización perezosa

De ahí sale un patrón que reaparece más adelante en el curso: crear algo caro solo la primera vez que se necesita.

ts

let conexion: Conexion | undefined; function obtenerConexion(): Conexion { return (conexion ??= crearConexion()); }

Definición — Cortocircuito: Que un operador no llegue a evaluar su lado derecho si el resultado ya está decidido por el izquierdo.

Ese es el detalle clave: ??= hace cortocircuito. Si la variable ya tiene valor, la expresión de la derecha ni siquiera se evalúa, así que crearConexion() no se llama por segunda vez. Es la diferencia con una asignación normal, y lo que hace que el patrón funcione.

Los tres operadores de asignación lógica

OperadorAsigna cuando el valor actual es??=null o undefined||=Falsy, lo que incluye 0 y ""&&=Truthy

La elección entre ellos es la misma decisión que entre || y ??: depende de si un valor vacío legítimo debe conservarse o no.

Ten en cuenta: los tres solo escriben cuando corresponde. Sobre una propiedad de solo lectura, ??= no falla si la propiedad ya tenía valor, porque no llega a asignar nada.


Ejercicios

Ejercicio 1: encontrar el bug

Este fragmento tiene tres defectos relacionados con los operadores de la lección. Localízalos y corrígelos.

Contexto: los tres campos son opcionales, y reintentos: 0, nota: '' y activo: false son valores que el usuario puede enviar a propósito.

ts

function construirRespuesta(opciones: { reintentos?: number; nota?: string; activo?: boolean; }) { return { reintentos: opciones.reintentos || 3, nota: opciones.nota || 'sin nota', activo: opciones.activo || true, }; }

Ejercicio 2: la cadena que no protege lo suficiente

Dado un objeto de pedido donde cliente puede faltar y, dentro de él, contacto también, escribe la expresión que obtenga el email sin que pueda lanzar en ningún caso y que devuelva un texto de relleno si falta cualquier nivel.

Después explica por qué pedido.cliente.contacto?.email no es suficiente.

Ejercicio 3: defaults idempotentes

Escribe una función configurar() que rellene con valores por defecto tres variables de entorno (PORT, LOG_LEVEL, TIMEOUT) sin pisar las que ya vengan definidas, incluso si la función se llama varias veces.

Ejercicio 4: inicialización perezosa

Escribe una función obtenerCatalogo() que construya un catálogo caro solo la primera vez que se la llama y devuelva siempre el mismo valor después. Explica en una frase por qué la expresión de la derecha no se evalúa en las llamadas posteriores.

Pista: la clave está en el cortocircuito, no en la asignación.


Soluciones

Solución 1

ts

function construirRespuesta(opciones: { reintentos?: number; nota?: string; activo?: boolean; }) { return { reintentos: opciones.reintentos ?? 3, nota: opciones.nota ?? 'sin nota', activo: opciones.activo ?? true, }; }

Los tres defectos son el mismo error aplicado a tres tipos distintos:

CampoValor legítimo que se perdíaQué pasabareintentos0Se convertía en 3nota''Se convertía en 'sin nota'activofalseSe convertía en true

El tercero es el más grave: desactivar algo de forma explícita quedaba activado.

Solución 2

ts

const email = pedido.cliente?.contacto?.email ?? 'sin-email';

pedido.cliente.contacto?.email no basta porque ?. solo protege el eslabón que tiene inmediatamente a su izquierda. En esa expresión protege el acceso a .email, pero el acceso a .contacto se hace sobre pedido.cliente sin ninguna protección, así que lanza si cliente falta.

Hace falta un ?. por cada nivel que pueda no existir.

Solución 3

ts

function configurar(): void { process.env.PORT ??= '3000'; process.env.LOG_LEVEL ??= 'info'; process.env.TIMEOUT ??= '5000'; }

En la segunda llamada no ocurre nada: las tres variables ya tienen valor, así que ninguna asignación se ejecuta. Con ||= el comportamiento sería distinto y peligroso, porque una variable definida como cadena vacía se sobrescribiría.

Solución 4

ts

let catalogo: Catalogo | undefined; function obtenerCatalogo(): Catalogo { return (catalogo ??= construirCatalogo()); }

??= hace cortocircuito: a partir de la segunda llamada, catalogo ya no es nullish, así que el operador decide el resultado sin evaluar su lado derecho y construirCatalogo() no se ejecuta.


Conclusión

Los tres operadores comparten un mismo criterio: actúan sobre lo nullish, no sobre lo falsy. Esa distinción es la que evita que un 0 o una cadena vacía legítimos se conviertan en el valor por defecto sin que nadie se entere.

?. evita el error al acceder pero deja un undefined que hay que resolver, normalmente con un ?? a continuación. Y ??= añade la idempotencia que permite asignar defaults e inicializar cosas caras una sola vez sin comprobaciones extra.

Cesar Enrique Manzano Velasco

Sobre Cesar Enrique Manzano Velasco

Ingeniero Full Stack & AI/ML practitioner. Builder en AtaraxiaTech. Construyo microSaaS con IA en producción y lo cuento todo en público.