Las propiedades de <task-card> declaradas hasta ahora usan tipos sencillos: String, Number y Boolean. Pero un componente real necesita a menudo manejar tipos de dato más ricos: colecciones, objetos estructurados, o incluso tipos que no existen de forma nativa en HTML, como una fecha. Esta lección repasa el catálogo completo de tipos que Lit sabe convertir de serie entre atributo y propiedad, se detiene en un problema clásico con Boolean, y enseña a escribir un conversor personalizado para un tipo propio, que se aplicará a una nueva propiedad fechaLimite de <task-card>.

Contenido

  1. El problema de fondo: HTML solo entiende texto
  2. Los tipos soportados de serie: String, Number, Boolean, Array, Object
  3. El problema clásico de Boolean con atributos
  4. Cuándo hace falta un conversor personalizado
  5. Escribiendo un converter a medida
  6. Aplicando el conversor a fechaLimite en <task-card>

  1. El problema de fondo: HTML solo entiende texto

Antes de entrar en el catálogo de tipos, conviene fijar la razón por la que existe todo este mecanismo de conversión. Un atributo HTML, por definición del propio estándar de la plataforma web, siempre es una cadena de texto. Cuando se escribe <task-card prioridad="5">, el navegador no almacena internamente el número 5; almacena literalmente el texto "5". Esto es así para cualquier atributo de cualquier elemento HTML, no solo para los Custom Elements: <input type="number" value="5"> también guarda "5" como texto en el atributo value, aunque el campo se comporte visualmente como numérico.

Sin embargo, en JavaScript interesa trabajar con los tipos de dato reales: un número de verdad para poder hacer operaciones aritméticas con prioridad, un booleano de verdad para poder usar urgente directamente en una condición, y así sucesivamente. La responsabilidad de Lit, cuando se declara type en static properties, es exactamente hacer de puente entre ambos mundos: convertir el texto del atributo al tipo JavaScript declarado quando el atributo cambia, y convertir el valor JavaScript de vuelta a texto cuando haga falta reflejarlo en el atributo (algo que se detalla en la siguiente lección, sobre reflexión).

  1. Los tipos soportados de serie: String, Number, Boolean, Array, Object

Lit reconoce, de fábrica, cinco valores para la opción type, cada uno con su propia lógica de conversión desde el texto del atributo:

type Cómo convierte de atributo (texto) a propiedad Ejemplo de atributo Valor resultante en JS
String Se usa el texto tal cual, sin transformar titulo="Revisar el PR" "Revisar el PR"
Number Se aplica Number(valorDelAtributo) prioridad="5" 5 (número)
Boolean Presencia/ausencia del atributo, no su contenido textual (se detalla en el apartado 3) urgente true
Array Se interpreta el texto del atributo como JSON y se parsea con JSON.parse etiquetas='["urgente","cliente"]' ['urgente', 'cliente']
Object Igual que Array, mediante JSON.parse sobre el texto del atributo metadatos='{"autor":"Ana"}' { autor: 'Ana' }

Los tres primeros tipos (String, Number, Boolean) son los que se han usado en <task-card> hasta ahora, y su comportamiento es razonablemente intuitivo salvo por el caso de Boolean, que se explica en el siguiente apartado. Los dos últimos (Array, Object) son menos habituales de establecer directamente como texto en un atributo HTML —escribir JSON válido a mano dentro de una etiqueta HTML es incómodo y propenso a errores de escapado de comillas—, pero resultan perfectamente naturales cuando la propiedad se asigna desde JavaScript, sin pasar en ningún momento por su representación en texto:

// Asignación directa desde JavaScript: no hace falta ningún JSON.parse manual,
// porque no se está pasando por la representación en texto del atributo.
tarjeta.etiquetas = ['urgente', 'cliente'];

De hecho, este es el patrón más habitual en aplicaciones reales construidas con Lit: los tipos compuestos (Array, Object) casi siempre se asignan desde JavaScript, mientras que los atributos HTML en el propio marcado se reservan sobre todo para tipos simples (String, Number, Boolean), que son los que tiene sentido escribir a mano en una plantilla HTML.

  1. El problema clásico de Boolean con atributos

El tipo Boolean merece una explicación aparte porque su comportamiento sorprende a quien lo ve por primera vez, y es una fuente frecuente de errores. La conversión de Boolean no mira el contenido textual del atributo, sino únicamente si el atributo está presente o ausente en la etiqueta, siguiendo la misma convención que usan los atributos booleanos nativos de HTML (disabled, checked, required...).

<!-- urgente vale true: el atributo está presente, con cualquier contenido o sin ninguno -->
<task-card urgente></task-card>
<task-card urgente="true"></task-card>
<task-card urgente="false"></task-card>
<task-card urgente=""></task-card>

<!-- urgente vale false: el atributo, sencillamente, no está -->
<task-card></task-card>

Este es, con diferencia, el error más habitual al trabajar con propiedades Boolean en Lit: escribir urgente="false" esperando que la propiedad valga false, cuando en realidad el atributo está presente (tiene el texto "false", pero está ahí), así que Lit lo interpreta como true. La única forma de que una propiedad Boolean valga false a través de un atributo HTML es que ese atributo no aparezca en absoluto en la etiqueta.

Esta convención no es una rareza inventada por Lit: es exactamente cómo funcionan los atributos booleanos nativos del navegador. <input disabled="false"> sigue deshabilitando el campo, porque el atributo disabled está presente; para habilitarlo, hay que quitar el atributo por completo, no ponerlo a "false". Lit simplemente respeta esta misma convención para mantener la coherencia con el resto de la plataforma web.

Desde JavaScript, en cambio, no existe esta ambigüedad: asignar tarjeta.urgente = false funciona exactamente como se espera, porque ahí no hay ningún texto de por medio, solo el valor booleano real de JavaScript.

  1. Cuándo hace falta un conversor personalizado

Los cinco tipos del apartado 2 cubren la inmensa mayoría de los casos habituales, pero existen tipos de dato que no encajan de forma natural en ninguno de ellos. El ejemplo que se va a usar en esta lección es una fecha: si <task-card> necesita una propiedad fechaLimite, ¿qué tipo debería declarar?

  • Declararla como String funcionaría a medias: el atributo funcionaría, pero la propiedad JavaScript sería simplemente una cadena de texto, sin ninguna de las capacidades de un objeto Date (comparar fechas, calcular cuántos días faltan, formatear de formas distintas).
  • Declararla como Object tampoco encaja bien: un objeto Date de JavaScript no se serializa de forma útil con JSON.stringify (produce una cadena en formato ISO dentro de comillas, pero al hacer JSON.parse de vuelta no se reconstruye automáticamente como un objeto Date, sino como una cadena de texto normal).

Para este tipo de situaciones, Lit permite sustituir la conversión automática basada en type por una función de conversión completamente a medida, mediante la opción converter.

  1. Escribiendo un converter a medida

Un converter es un objeto con hasta dos funciones: fromAttribute, que convierte el texto del atributo al valor de la propiedad, y toAttribute, que hace el camino inverso (necesaria solo si además se usa reflect: true, que se explica en la siguiente lección). Para el caso de fechaLimite, interesa convertir entre una cadena de texto en formato AAAA-MM-DD (el formato habitual de un atributo de fecha en HTML, el mismo que usa <input type="date">) y un objeto Date real de JavaScript:

const conversorDeFecha = {
  fromAttribute(valorDelAtributo) {
    // valorDelAtributo es el texto del atributo, o null si no está presente
    if (!valorDelAtributo) {
      return null;
    }
    return new Date(valorDelAtributo);
  },
  toAttribute(valorDeLaPropiedad) {
    // valorDeLaPropiedad es el objeto Date (o null) de la propiedad
    if (!valorDeLaPropiedad) {
      return null;
    }
    return valorDeLaPropiedad.toISOString().split('T')[0]; // "AAAA-MM-DD"
  },
};

Analicemos cada función por separado. fromAttribute recibe el texto del atributo tal cual está en el HTML (por ejemplo, "2026-07-15") y debe devolver el valor que tendrá la propiedad JavaScript: aquí, un objeto Date construido con new Date(valorDelAtributo), que interpreta correctamente una cadena en formato AAAA-MM-DD. toAttribute hace el camino contrario: recibe el objeto Date de la propiedad y debe devolver el texto que se escribirá en el atributo si la propiedad se refleja; aquí se usa toISOString() (que devuelve una fecha completa con hora, en formato 2026-07-15T00:00:00.000Z) y se recorta con split('T')[0] para quedarse solo con la parte de la fecha.

Ambas funciones comprueban primero si el valor de entrada es "falsy" (null, undefined, cadena vacía) y devuelven null en ese caso, para evitar errores si la propiedad todavía no tiene fecha asignada; es una precaución razonable en cualquier conversor personalizado, ya que Lit puede llamar a estas funciones en momentos en los que el valor todavía no se ha establecido.

  1. Aplicando el conversor a fechaLimite en <task-card>

Con el conversor ya escrito, se aplica a la propiedad indicándolo en el objeto de configuración de static properties, en lugar de (o junto a) type:

import { LitElement, html } from 'lit';

const conversorDeFecha = {
  fromAttribute(valorDelAtributo) {
    if (!valorDelAtributo) {
      return null;
    }
    return new Date(valorDelAtributo);
  },
  toAttribute(valorDeLaPropiedad) {
    if (!valorDeLaPropiedad) {
      return null;
    }
    return valorDeLaPropiedad.toISOString().split('T')[0];
  },
};

class TaskCard extends LitElement {
  static properties = {
    titulo: { type: String },
    estado: { type: String },
    prioridad: { type: Number },
    urgente: { type: Boolean },
    expandida: { state: true },
    fechaLimite: { converter: conversorDeFecha, attribute: 'fecha-limite' },
  };

  constructor() {
    super();
    this.titulo = 'Tarea sin título';
    this.estado = 'pendiente';
    this.prioridad = 3;
    this.urgente = false;
    this.expandida = false;
    this.fechaLimite = null;
  }

  renderInsigniaEstado() {
    if (this.estado === 'hecha') {
      return html`<span class="insignia insignia--hecha">✓ Hecha</span>`;
    }
    if (this.estado === 'en-progreso') {
      return html`<span class="insignia insignia--progreso">◐ En progreso</span>`;
    }
    return html`<span class="insignia insignia--pendiente">○ Pendiente</span>`;
  }

  renderFechaLimite() {
    if (!this.fechaLimite) {
      return '';
    }
    return html`<p>Fecha límite: ${this.fechaLimite.toLocaleDateString('es-ES')}</p>`;
  }

  render() {
    return html`
      <article>
        <h3>${this.titulo}</h3>
        ${this.renderInsigniaEstado()}
        <p>Prioridad: ${this.prioridad}</p>
        ${this.renderFechaLimite()}
        ${this.urgente && html`<p class="aviso">⚠ Urgente</p>`}
      </article>
    `;
  }
}

customElements.define('task-card', TaskCard);

Con esta declaración, <task-card fecha-limite="2026-07-15"> hace que this.fechaLimite sea, dentro del componente, un objeto Date real, no una cadena de texto: se puede llamar a this.fechaLimite.toLocaleDateString('es-ES') (como se hace en renderFechaLimite) o a cualquier otro método de Date, sin necesidad de convertir nada manualmente en render(). Esto es exactamente el valor de un conversor personalizado: traslada la responsabilidad de la conversión al único sitio donde se declara la propiedad, en lugar de repetirla cada vez que se lee el valor.

Nótese también que se ha indicado explícitamente attribute: 'fecha-limite', aplicando lo aprendido en la primera lección del módulo sobre nombres de atributo en kebab-case para propiedades con nombre compuesto en camelCase.

Errores Comunes y Consejos

  • Escribir urgente="false" esperando que la propiedad valga false: como se explicó en el apartado 3, con type: Boolean lo que importa es la presencia o ausencia del atributo, no su contenido de texto; para que valga false desde HTML, el atributo debe omitirse por completo.
  • Intentar declarar type: Date esperando que Lit lo soporte de serie: Date no es uno de los cinco tipos reconocidos automáticamente (apartado 2); si se declara { type: Date }, Lit lo tratará de forma equivalente a String sin ningún tipo de conversión especial, y this.fechaLimite sería simplemente el texto del atributo, no un objeto Date. Para tipos no soportados de serie hace falta un converter personalizado, como se ha visto en el apartado 5.
  • Olvidar comprobar valores nulos o vacíos dentro de fromAttribute/toAttribute: si el conversor del apartado 5 no comprobara if (!valorDelAtributo) al principio, llamar a new Date(null) o new Date(undefined) produciría una fecha inválida (Invalid Date) en lugar de fallar de forma clara o devolver null, lo que puede provocar errores confusos más adelante en la plantilla.
  • Pasar JSON mal formado a un atributo de tipo Array u Object: escribir a mano etiquetas='["urgente", "cliente"]' dentro de un atributo HTML es propenso a errores de comillas (las comillas dobles del JSON chocan con las comillas del propio atributo si no se gestionan con cuidado); en la práctica, para tipos compuestos suele ser más fiable asignar la propiedad desde JavaScript, como se apuntó en el apartado 2.

Ejercicios

  1. Sin usar ningún conversor personalizado, declara en <task-card> una propiedad etiquetas de tipo Array, con valor inicial [], y muéstrala en la plantilla como una lista separada por comas (this.etiquetas.join(', ')). Comprueba, asignándola desde la consola del navegador con elemento.etiquetas = ['cliente', 'urgente'], que la plantilla se actualiza.
  2. Retoma el conversor conversorDeFecha del apartado 5 y añade una comprobación adicional en fromAttribute que, si el new Date(...) resultante fuera inválido (se puede comprobar con Number.isNaN(fecha.getTime())), devuelva null en lugar de una fecha inválida.
  3. Explica con tus propias palabras por qué <task-card urgente="false"></task-card> no deshabilita el aviso de urgencia, y qué habría que escribir en su lugar para que urgente valiera false.

Soluciones

static properties = {
  // ...propiedades anteriores...
  etiquetas: { type: Array },
};

constructor() {
  super();
  // ...
  this.etiquetas = [];
}

render() {
  return html`
    <article>
      <h3>${this.titulo}</h3>
      <p>Etiquetas: ${this.etiquetas.join(', ')}</p>
    </article>
  `;
}

Al ejecutar elemento.etiquetas = ['cliente', 'urgente'] desde la consola, se reasigna por completo la propiedad etiquetas (no se muta el array existente), así que el setter reactivo se dispara con normalidad y la plantilla muestra "Etiquetas: cliente, urgente".

const conversorDeFecha = {
  fromAttribute(valorDelAtributo) {
    if (!valorDelAtributo) {
      return null;
    }
    const fecha = new Date(valorDelAtributo);
    if (Number.isNaN(fecha.getTime())) {
      return null;
    }
    return fecha;
  },
  toAttribute(valorDeLaPropiedad) {
    if (!valorDeLaPropiedad) {
      return null;
    }
    return valorDeLaPropiedad.toISOString().split('T')[0];
  },
};
  1. Con type: Boolean, Lit determina el valor de la propiedad únicamente por si el atributo está presente en la etiqueta, sin fijarse en su contenido de texto; como urgente="false" sigue teniendo el atributo urgente presente (con el texto "false" dentro, pero presente al fin y al cabo), Lit lo interpreta como true, igual que ocurre con atributos nativos como disabled. Para que urgente valga false habría que escribir la etiqueta sin el atributo en absoluto: <task-card></task-card>.

Conclusión

En esta lección has recorrido el catálogo completo de tipos que Lit convierte de serie entre atributo y propiedad (String, Number, Boolean, Array, Object), te has detenido en el comportamiento particular de Boolean con atributos —presencia frente a contenido textual—, y has aprendido a escribir un converter personalizado para tipos que no encajan en ese catálogo, aplicándolo a una nueva propiedad fechaLimite de <task-card> que ahora maneja objetos Date reales en lugar de simples cadenas de texto.

Todo este mecanismo de conversión da por supuesto un flujo en un solo sentido: del atributo hacia la propiedad. En la siguiente lección, "Atributos vs Propiedades y Reflexión", cerrarás el círculo entendiendo también el camino inverso —cuándo y por qué interesa que un cambio en la propiedad se refleje de vuelta en el atributo—, y aplicarás todo lo aprendido en este módulo para convertir tareas, en <task-list>, en una propiedad reactiva de tipo Array, consiguiendo por fin que cada <task-card> del tablero muestre los datos de su propia tarea.

Curso de Lit

Módulo 1: Introducción a Lit y Web Components

Módulo 2: Plantillas Reactivas y Renderizado

Módulo 3: Propiedades y Estado Reactivo

Módulo 4: Estilos en Componentes Lit

Módulo 5: Eventos y Comunicación entre Componentes

Módulo 6: Ciclo de Vida y Comportamiento Avanzado

Módulo 7: Directivas y Funcionalidades Avanzadas de Plantillas

Módulo 8: Integración, Interoperabilidad y Despliegue

Módulo 9: Pruebas y Buenas Prácticas

Módulo 10: Proyecto: Construyendo TaskFlow

© Copyright 2026. Todos los derechos reservados