wandres.dev
PROPS Y SLOTS · componer componentes

Props opcionales, valores por defecto y rest

Cómo marcar props opcionales con el modificador de interrogación, dar valores por defecto al desestructurar Astro.props, recoger props sobrantes con rest, y reenviarlas a un elemento con spread para construir componentes envoltorio transparentes.

⏱ 14 min

Un contrato de props rígido obliga al padre a repetir siempre los mismos valores. El diseño maduro de un componente distingue lo obligatorio de lo prescindible: props que deben venir, props que pueden faltar, y un valor sensato cuando faltan. Astro combina el sistema de tipos de TypeScript con la desestructuración de JavaScript para expresar esos matices, y con rest y spread permite que un componente acepte y reenvíe props que ni siquiera nombra.

🎯 Al terminar esta lección sabrás
  • Declarar props opcionales con el modificador ? en la interface Props.
  • Asignar valores por defecto al desestructurar Astro.props.
  • Recoger props no declaradas con la sintaxis rest y el operador ....
  • Reenviar esas props a un elemento con spread para crear envoltorios transparentes.

Props opcionales: el modificador de interrogación

Por defecto, cada prop de una interface Props es obligatoria: omitirla es un error de tipos. Para declarar que una prop puede faltar, se añade el modificador ? a su nombre. El tipo resultante incluye undefined, y el compilador deja de exigirla.

---
interface Props {
  titulo: string;
  subtitulo?: string;
}
const { titulo, subtitulo } = Astro.props;
---
<h2>{titulo}</h2>
{subtitulo && <p>{subtitulo}</p>}

Marcar subtitulo como opcional tiene una consecuencia inmediata en el hijo: su tipo es string | undefined, así que no puedes tratarlo como texto sin comprobar antes que existe. Por eso el ejemplo lo protege con subtitulo && ..., que solo renderiza el párrafo cuando hay valor. Lo opcional en el contrato obliga a lo defensivo en el uso.

Valores por defecto: la red de seguridad

Una prop opcional que llega como undefined deja un hueco. A menudo prefieres un valor sensato en su lugar, y el sitio idiomático para fijarlo es la propia desestructuración de Astro.props.

---
interface Props {
  variante?: 'solida' | 'fantasma';
  nivel?: number;
}
const { variante = 'solida', nivel = 1 } = Astro.props;
---
<div class={`btn-${variante}`} data-nivel={nivel}>
  <slot />
</div>

El valor por defecto se aplica exactamente cuando la prop es undefined, ni antes ni después. Si el padre pasa variante="fantasma", gana su valor; si la omite, entra solida. Combinar el ? en la interfaz con el = en la desestructuración es el patrón canónico: la interfaz declara que la prop puede faltar, y la desestructuración decide qué ocurre cuando falta.

⚠️
undefined activa el defecto, null no

El valor por defecto salta solo ante undefined. Si el padre pasa explícitamente null, ese null sobrevive: la desestructuración no lo reemplaza. Por eso conviene modelar la ausencia con undefined u omitiendo la prop, no con null, salvo que quieras distinguir deliberadamente los dos casos.

Rest: recoger lo que no nombras

A veces un componente no quiere enumerar todas las props que acepta. Un botón envoltorio, por ejemplo, debería admitir cualquier atributo válido de un botón nativo —type, disabled, aria-label— sin listarlos uno a uno. La sintaxis rest recoge en un solo objeto todas las props que no desestructuraste por nombre.

---
import type { HTMLAttributes } from 'astro/types';
interface Props extends HTMLAttributes<'button'> {
  variante?: 'solida' | 'fantasma';
}
const { variante = 'solida', class: clase, ...resto } = Astro.props;
---
<button class:list={[`btn-${variante}`, clase]} {...resto}>
  <slot />
</button>

Dos piezas se combinan aquí. Extender HTMLAttributes<'button'> hace que la interfaz herede todos los atributos legítimos de un botón, tipados; así el padre recibe autocompletado de disabled o type sin que tú los declares. Y ...resto captura justo esos atributos heredados que no extrajiste a mano, dejándolos listos para reenviar.

📝
class es palabra reservada

Al desestructurar no puedes escribir const { class } = Astro.props, porque class es palabra reservada de JavaScript. Se renombra en la propia desestructuración: class: clase guarda el valor de la prop class en una variable llamada clase. Es un tropiezo habitual con la prop más común de todas.

Spread: reenviar props a un elemento

Recoger props con rest solo es útil si luego las colocas en algún sitio. El operador spread {...resto} las derrama sobre un elemento, convirtiendo cada clave del objeto en un atributo. Es la mitad receptora del patrón: lo que rest recoge, spread reparte.

flowchart LR
PAD[Padre pasa type y disabled y class] --> HIJO[Componente Boton]
HIJO --> DES[Desestructura variante y class]
DES --> REST[resto recoge type y disabled]
REST --> SPREAD[spread los vierte en el button]
style PAD fill:#89b4fa,color:#11111b
style SPREAD fill:#a6e3a1,color:#11111b

El resultado es un envoltorio transparente: un componente que añade su valor —la clase btn-solida, la lógica de la variante— sin bloquear los atributos nativos del elemento que envuelve. El padre lo usa casi como usaría un <button> normal, pero con las mejoras que el componente aporta. La clase se fusiona con class:list, que combina la clase propia del componente con la que venga de fuera sin que una pise a la otra.

Este patrón escala a cualquier elemento: un <input> que reenvía sus atributos, un enlace que hereda los de un ancla, una imagen que acepta los de <img>. En todos, la receta es la misma: extiende HTMLAttributes del elemento, extrae por nombre lo que el componente gestiona, y reenvía el ...resto con spread.

Un buen componente no captura, deja pasar

El instinto al diseñar un componente es cerrarlo: declarar solo las props que se te ocurren y rechazar todo lo demás. Rest y spread invitan a la disciplina contraria, y en ella hay una lección de diseño profunda. Un envoltorio verdaderamente reutilizable no intenta anticipar cada atributo que alguien querrá ponerle; se limita a interceptar lo que de verdad gestiona —una variante, una clase que fusionar— y deja pasar intacto todo lo que no le incumbe. Extender HTMLAttributes es un acto de humildad tipada: reconoce que el elemento nativo ya sabe recibir decenas de atributos que tu componente no debería reimplementar ni obstruir. Spread es la promesa de no interponerse. Juntos convierten tu componente en una capa fina y honesta sobre la plataforma, en lugar de una jaula que reencierra lo que el navegador ya ofrecía. La consecuencia es doble. Para quien lo usa, el componente se siente natural: acepta aria-label, data-* o disabled sin que hayas previsto ninguno, porque no los bloqueas. Para quien lo mantiene, el contrato se mantiene mínimo: declaras solo tu aporte real y delegas el resto a la plataforma, que no vas a mejorar. Diseñar así —capturar lo justo, reenviar lo demás, fusionar en vez de sobrescribir— es la diferencia entre un componente que la gente pelea por adaptar y uno que se adapta solo. La transparencia no es dejadez: es el reconocimiento de que la mejor abstracción es la que añade valor sin quitar libertad.

⚔️ Construye un envoltorio transparente
  1. Crea Boton.astro con una interface Props que extienda HTMLAttributes<'button'> y añada una prop opcional variante con valor por defecto.
  2. Desestructura variante, la prop class renombrada y ...resto; reenvía ...resto al <button> y fusiona clases con class:list.
  3. Úsalo pasando disabled y un aria-label; inspecciona el HTML y confirma que ambos llegaron al botón sin que los declararas.
  4. Pasa class="extra" desde el padre y verifica que se combina con la clase de variante en vez de reemplazarla.