Desestructuración, spread y propiedades computadas
Lección 2 de 43 · El lenguaje: de JavaScript a TypeScript estricto

De JavaScript a TypeScript estrictoLección 2/7
Temario del curso
De JavaScript a TypeScript estricto
Lección 2 de 7
CursoDe JavaScript a TypeScript estricto · Lección 2 de 7
La lección anterior trató de cómo entra y sale el código de un módulo. Esta trata de qué haces con los datos una vez dentro: sacarlos de un objeto o de un array y volver a componerlos.
Son tres construcciones que aparecen en casi cualquier archivo de un servicio real. Las tres se aprenden en cinco minutos, y las tres tienen un matiz que provoca bugs cuando se usan por costumbre en vez de por criterio.
Nota sobre la terminología: en este artículo, literal de objeto significa un objeto escrito directamente con llaves, como { id: 1 }. Y copia superficial significa una copia que duplica solo el primer nivel de propiedades.
Desestructuración: sacar datos por nombre y por posición
Definición — Desestructuración: Sintaxis que declara varias variables a partir de la forma de un objeto o de un array, en una sola sentencia, en vez de acceder campo a campo.
Sintaxis básica
La versión larga y la corta hacen exactamente lo mismo:
ts
// versión larga
const nombre = usuario.nombre;
const email = usuario.email;
// desestructuración
const { nombre, email } = usuario;La ganancia no es solo escribir menos. La línea declara de un vistazo qué partes del objeto usa el código a continuación, y eso es información útil cuando lees una función ajena.
En objetos se extrae por nombre. En arrays, por posición:
ts
const [primero, segundo] = ['a', 'b', 'c']; // 'a', 'b'
const [, tercero] = ['a', 'b', 'c']; // se salta posiciones con comasTypeScript no necesita ayuda aquí: cada variable recibe el tipo del campo o de la posición que le corresponde. Anotarlas a mano no aporta nada y suele empeorar la lectura.
Con la sintaxis básica clara, vamos a los dos añadidos que la versión larga no tiene.
Renombrado
Los dos puntos introducen el nombre local de la variable. Es la fuente habitual de confusión, porque : en cualquier otro sitio de TypeScript introduce un tipo, y aquí no.
ts
const { metodo: metodoPago } = body;
// se lee: toma el campo `metodo` y llámalo `metodoPago` en este ámbitoValores por defecto
El signo = da un valor de reserva. Se aplica solo cuando el campo vale undefined:
Valor de config.reintentosResultado de const { reintentos = 3 } = config55undefined3nullnull — el default NO se aplica00
Ese detalle es la causa de un bug frecuente. Un campo que llega como null desde una base de datos o desde JSON atraviesa el valor por defecto sin activarlo, y el null sigue viajando por el código como si fuera un valor legítimo.
Ambas cosas se combinan, y el orden es siempre el mismo: origen, nombre local, default.
ts
const { orderId, metodo: metodoPago = 'card' } = body;Desestructurar en los parámetros de una función
El mismo patrón funciona en la firma de una función. Así recibes un objeto de opciones y nombras sus campos en el mismo sitio donde se declaran:
ts
function crearPedido({ id, nota = '' }: { id: string; nota?: string }): void {
/* ... */
}Si el objeto entero puede no llegar, necesitas un default para el propio parámetro. Sin él, desestructurar undefined lanza un error en ejecución:
ts
function conectar({ timeout = 5000 } = {}): void { /* ... */ }
conectar(); // funciona: el `= {}` evita desestructurar undefinedTen en cuenta: desestructurar en la firma tiene un coste. El objeto original deja de tener nombre dentro de la función, así que no puedes reenviarlo entero ni loguearlo completo. Si el cuerpo necesita ambas cosas, recibe el objeto con nombre y desestructura dentro.
Anidamiento: hasta dónde llegar
La desestructuración anida, pero anidar demasiado convierte una línea legible en un jeroglífico. Y esconde un riesgo: si un nivel intermedio no existe, la línea falla en ejecución.
ts
const { cliente: { email } } = pedido; // si `cliente` es undefined, esto lanzaCon dos niveles suele seguir leyéndose bien. A partir de ahí, extrae el nivel intermedio a su propia variable y compruébalo.
Spread: copia superficial y composición
Definición — Spread: El operador ... colocado dentro de un literal. Expande ahí los elementos de un array o las propiedades propias de un objeto.
Sintaxis básica
Su uso más frecuente es crear un valor nuevo a partir de uno existente, en vez de modificar el original:
ts
const conFecha = { ...order, updatedAt: new Date() };Esa línea produce un objeto nuevo con todos los campos de order más updatedAt. El objeto order no se toca. Trabajar así, creando en vez de modificando, evita una familia entera de bugs: los que aparecen cuando una función cambia un objeto que otra parte del código seguía usando.
En arrays funciona igual, concatenando en vez de fusionando:
ts
const todos = [...anteriores, nuevo];El orden decide quién gana
Dentro de un literal, lo que se escribe después pisa a lo anterior:
ts
const conDefaults = { reintentos: 3, ...config }; // config puede pisar el 3
const forzado = { ...config, reintentos: 3 }; // reintentos siempre vale 3Qué copia y qué comparte
Este es el punto que más caro sale, y merece su propia definición.
Definición — Copia superficial: Una copia que duplica solo el primer nivel de propiedades. Si el valor de una propiedad es un objeto o un array, lo que se copia es la referencia, no el contenido.
El spread hace una copia superficial. La consecuencia es que la copia y el original siguen apuntando al mismo objeto anidado:
ts
const original = { id: 1, etiquetas: ['a'] };
const copia = { ...original };
copia.id = 2; // afecta solo a la copia
copia.etiquetas.push('b'); // afecta a AMBOS: es el mismo array
original.etiquetas; // ['a', 'b'] ← el original cambióLlevado a un caso real: copias un pedido con spread, modificas una de sus líneas, y estás modificando también el pedido original, aunque el código dé toda la impresión de trabajar sobre una copia independiente.
Para copiar también los niveles internos, hazlo de forma explícita en cada nivel que vayas a modificar:
ts
const copiaSegura = { ...original, etiquetas: [...original.etiquetas] };El atajo: el spread copia un nivel. Antes de escribirlo, pregúntate "¿voy a modificar algo que esté más adentro?". Si la respuesta es sí, ese nivel también hay que copiarlo. Para estructuras profundas existe
structuredClone(valor), disponible en Node moderno, aunque no clona funciones ni instancias de clase con métodos.
Dos detalles más
El spread copia solo propiedades propias y enumerables. Los métodos vienen del prototipo, así que no se copian. Por eso { ...instanciaDeUnaClase } produce un objeto plano sin métodos, que además responde false a instanceof.
El tipo lo infiere el compilador solo. El spread de un objeto tipado produce un tipo con todos sus campos, y combinarlo con un literal produce la unión de ambas formas. No suele hacer falta anotar el resultado.
Rest: recoger lo que sobra
Definición — Patrón rest: El mismo ..., pero en el lado izquierdo de una asignación o en una firma de función. En vez de expandir, recoge en una sola variable todo lo que no se extrajo por nombre o por posición.
Ya vimos el spread, que reparte. El rest hace lo contrario: agrupa.
En objetos, recoge los campos restantes:
ts
const { password, ...resto } = usuario;
// `resto` tiene todos los campos de `usuario` menos `password`Ese patrón es la forma idiomática de excluir un campo sin modificar el original. Se usa para no devolver una contraseña en una respuesta HTTP, o para no volcar un campo sensible en un log.
En arrays, recoge la cola:
ts
const [primero, ...siguientes] = registros;Y en una firma de función, recoge los argumentos restantes. Va siempre en última posición y siempre llega como array:
ts
function log(nivel: string, ...partes: string[]): void {
console.log(nivel, partes.join(' '));
}Ten en cuenta: el rest de objeto también es superficial.
restocomparte las referencias anidadas con el original, igual que el spread.
Propiedades computadas: claves que se deciden en ejecución
Definición — Propiedad computada: Una clave de objeto escrita entre corchetes, cuyo valor se calcula en tiempo de ejecución en vez de escribirse literalmente.
Normalmente la clave de un objeto se escribe tal cual. Cuando no la conoces hasta que el programa corre, porque viene de una variable o del resultado de una expresión, la envuelves entre corchetes:
ts
const columna = { [`col_${nombre}`]: valor };Antes de que existiera esta sintaxis había que crear el objeto vacío y asignar después. Eso obligaba a empezar con un objeto mutable en vez de declararlo completo de una vez.
El uso más frecuente es construir un objeto a partir de datos:
ts
const porId = { [registro.id]: registro };También se combina con el spread para actualizar el campo que corresponda:
ts
const actualizado = { ...estado, [campo]: valor };Lo que aporta TypeScript aquí
El tipo del resultado depende de lo que el compilador sepa de la clave. Y eso decide si podrá seguir comprobando algo o no:
Tipo de la claveTipo del objeto resultante¿El compilador comprueba los accesos?stringObjeto de claves abiertasNoLiteral, como 'estado'{ estado: ... }Sí
ts
const clave: string = obtenerClave();
const abierto = { [clave]: 1 }; // claves abiertas: nada que comprobar
const fija = 'estado' as const;
const cerrado = { [fija]: 'ENVIADO' }; // { estado: string }Adelanto: cuando las claves posibles forman un conjunto cerrado y conocido, conviene que el tipo lo refleje, porque eso permite que el compilador exija cubrirlas todas. Una clave
stringabierta renuncia a esa comprobación. Es el tema de la lección sobreRecorde index signatures.
Ejercicios
Ejercicio 1: desestructurar con renombrado y default
Reescribe estas cuatro líneas en una sola sentencia de desestructuración, manteniendo el mismo comportamiento.
Contexto: body es un objeto con los campos orderId, metodo, nota y lineas, donde metodo y nota pueden faltar.
ts
const orderId = body.orderId;
const metodoPago = body.metodo === undefined ? 'card' : body.metodo;
const nota = body.nota;
const primeraLinea = body.lineas[0];Ejercicio 2: la copia que no era copia
Escribe un fragmento que demuestre el problema de la copia superficial: crea un objeto con una propiedad anidada, cópialo con spread, modifica esa propiedad en la copia y comprueba que el original cambió. Después arréglalo para que la copia sea independiente.
Ejercicio 3: excluir un campo sin modificar el original
Escribe una función sinDatosSensibles que reciba un objeto de pedido con los campos id, cliente, total y documentoCliente, y devuelva un objeto nuevo sin documentoCliente.
Condiciones: no modificar el original y no enumerar a mano los campos que sí se conservan.
Pista: el patrón rest de objeto hace exactamente esto en una línea.
Ejercicio 4: agrupar con claves dinámicas
Dado un array de líneas de pedido con la forma { sku: string; cantidad: number }, construye un objeto que use el sku como clave y la cantidad como valor, sin modificar ningún objeto intermedio.
Después, anota qué tipo infiere el compilador para el resultado y explica por qué.
Soluciones
Solución 1
ts
const { orderId, metodo: metodoPago = 'card', nota, lineas: [primeraLinea] } = body;El último campo combina las dos formas: se extrae lineas y, en la misma expresión, se desestructura por posición para quedarse con el primer elemento.
Ojo con el default de metodoPago: el ternario original comparaba explícitamente con undefined, así que el comportamiento coincide. Si el original hubiera usado ||, la traducción no sería equivalente, porque || también reemplaza la cadena vacía.
Solución 2
ts
const original = { id: 1, etiquetas: ['a'] };
const copia = { ...original };
copia.etiquetas.push('b');
console.log(original.etiquetas); // ['a', 'b'] — el original cambió
// arreglado: se copia también el nivel interno
const copiaSegura = { ...original, etiquetas: [...original.etiquetas] };
copiaSegura.etiquetas.push('c');
console.log(original.etiquetas); // ['a', 'b'] — intactoEl spread copió id por valor, porque es un número, y etiquetas por referencia, porque es un array. Solo el segundo caso comparte estado.
Solución 3
ts
function sinDatosSensibles(pedido: {
id: string;
cliente: string;
total: number;
documentoCliente: string;
}) {
const { documentoCliente, ...resto } = pedido;
return resto;
}La desestructuración extrae el campo que se quiere descartar y el rest recoge todo lo demás. El objeto original no se toca, porque resto es un objeto nuevo.
Solución 4
ts
const unidadesPorSku = lineas.reduce(
(acc, linea) => ({ ...acc, [linea.sku]: linea.cantidad }),
{} as Record<string, number>,
);El tipo del resultado es un objeto de claves abiertas de string a number. La razón es que linea.sku tiene tipo string, no un literal, así que el compilador no puede saber qué claves acabará teniendo el objeto ni comprobar los accesos posteriores.
reduce se explica en detalle en la lección 4; aquí lo que importa es de dónde sale ese tipo.
Conclusión
Estas tres construcciones se dominan cuando interiorizas sus tres matices:
-
El valor por defecto de una desestructuración solo cubre
undefined, nonull. -
El spread copia un único nivel, y los objetos anidados se comparten.
-
Una clave computada de tipo
stringrenuncia a que el compilador compruebe nada sobre ella.
El resto es sintaxis. Esos tres detalles son los que separan un código que compone datos con seguridad de uno que arrastra un null o comparte un array sin darse cuenta.
Artículos relacionados

Variables, constantes y tipos primitivos
Con esta lección empieza el bloque del sistema de tipos, y el punto de partida es el más pequeño posible: declarar una variable. Parece trivial, pero la forma de declararla decide dos cosas distintas. Qué se puede reasignar, y qué tipo infiere el compilador. Ambas tienen consecuencias que se arrastran por el resto del código. Nota sobre la terminología: en este artículo, declarar significa crear una variable con let, const o var. Asignar significa darle un valor, y reasignar significa cambiar el valor al que apunta una variable ya declarada.

JSON.parse y la frontera sin tipos
Las cinco lecciones anteriores trataron datos que ya estaban dentro del programa. Esta trata del momento en que entran. JSON es el formato por el que llega casi todo lo externo: el cuerpo de una petición HTTP, el mensaje de una cola, la respuesta de un servicio de terceros, un archivo de configuración. Y JSON.parse es la puerta por la que pasa. Esa puerta tiene una particularidad incómoda: el compilador no puede comprobar nada de lo que la cruza, y por defecto tampoco avisa de que no puede. Nota sobre la terminología: en este artículo, serializar significa convertir un valor a texto JSON, y parsear significa el proceso inverso. Frontera del sistema es cualquier punto donde entran datos que el programa no generó.

Operadores de ausencia: ?., ?? y ??=
Lección 5 de 43 · El lenguaje: de JavaScript a TypeScript estricto