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
- El problema de fondo: HTML solo entiende texto
- Los tipos soportados de serie:
String,Number,Boolean,Array,Object - El problema clásico de
Booleancon atributos - Cuándo hace falta un conversor personalizado
- Escribiendo un
convertera medida - Aplicando el conversor a
fechaLimiteen<task-card>
- 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).
- Los tipos soportados de serie:
String, Number, Boolean, Array, Object
String, Number, Boolean, Array, ObjectLit 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.
- El problema clásico de
Boolean con atributos
Boolean con atributosEl 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.
- 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
Stringfuncionarí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 objetoDate(comparar fechas, calcular cuántos días faltan, formatear de formas distintas). - Declararla como
Objecttampoco encaja bien: un objetoDatede JavaScript no se serializa de forma útil conJSON.stringify(produce una cadena en formato ISO dentro de comillas, pero al hacerJSON.parsede vuelta no se reconstruye automáticamente como un objetoDate, 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.
- Escribiendo un
converter a medida
converter a medidaUn 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.
- Aplicando el conversor a
fechaLimite en <task-card>
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 valgafalse: como se explicó en el apartado 3, contype: Booleanlo que importa es la presencia o ausencia del atributo, no su contenido de texto; para que valgafalsedesde HTML, el atributo debe omitirse por completo. - Intentar declarar
type: Dateesperando que Lit lo soporte de serie:Dateno es uno de los cinco tipos reconocidos automáticamente (apartado 2); si se declara{ type: Date }, Lit lo tratará de forma equivalente aStringsin ningún tipo de conversión especial, ythis.fechaLimitesería simplemente el texto del atributo, no un objetoDate. Para tipos no soportados de serie hace falta unconverterpersonalizado, 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 comprobaraif (!valorDelAtributo)al principio, llamar anew Date(null)onew Date(undefined)produciría una fecha inválida (Invalid Date) en lugar de fallar de forma clara o devolvernull, lo que puede provocar errores confusos más adelante en la plantilla. - Pasar JSON mal formado a un atributo de tipo
ArrayuObject: escribir a manoetiquetas='["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
- Sin usar ningún conversor personalizado, declara en
<task-card>una propiedadetiquetasde tipoArray, 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 conelemento.etiquetas = ['cliente', 'urgente'], que la plantilla se actualiza. - Retoma el conversor
conversorDeFechadel apartado 5 y añade una comprobación adicional enfromAttributeque, si elnew Date(...)resultante fuera inválido (se puede comprobar conNumber.isNaN(fecha.getTime())), devuelvanullen lugar de una fecha inválida. - 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 queurgentevalierafalse.
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];
},
};- 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; comourgente="false"sigue teniendo el atributourgentepresente (con el texto"false"dentro, pero presente al fin y al cabo), Lit lo interpreta comotrue, igual que ocurre con atributos nativos comodisabled. Para queurgentevalgafalsehabrí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
- ¿Qué son los Web Components y por qué Lit?
- Configuración del Entorno de Desarrollo
- Tu Primer Componente Lit
- Anatomía de un Componente Lit
Módulo 2: Plantillas Reactivas y Renderizado
- El Motor de Plantillas de Lit
- Expresiones e Interpolación en Plantillas
- Renderizado Condicional
- Renderizado de Listas
- El Ciclo de Renderizado
Módulo 3: Propiedades y Estado Reactivo
- Propiedades Reactivas
- Estado Interno con @state
- Tipos de Propiedades y Conversores Personalizados
- Atributos vs Propiedades y Reflexión
Módulo 4: Estilos en Componentes Lit
- CSS Encapsulado con Shadow DOM
- Estilos Compartidos entre Componentes
- Variables CSS Personalizadas y Theming
- Slots y Estilizado de Contenido Distribuido
Módulo 5: Eventos y Comunicación entre Componentes
- Manejo de Eventos DOM en Plantillas
- Eventos Personalizados: Comunicación Hijo a Padre
- Comunicación de Padre a Hijo con Propiedades
- Patrones de Comunicación entre Componentes Hermanos
Módulo 6: Ciclo de Vida y Comportamiento Avanzado
- Callbacks del Ciclo de Vida
- Hooks Reactivos: willUpdate, updated y firstUpdated
- Controladores Reactivos
- Mixins y Composición de Comportamiento
Módulo 7: Directivas y Funcionalidades Avanzadas de Plantillas
- Directivas Incorporadas: classMap, styleMap e ifDefined
- Directivas Personalizadas
- Renderizado Asíncrono con until
- Contexto Compartido con @lit/context
Módulo 8: Integración, Interoperabilidad y Despliegue
- Usar Componentes Lit en HTML Plano
- Integrar Lit con React, Vue y Angular
- Renderizado en el Servidor con @lit-labs/ssr
- Empaquetado, Publicación y TypeScript
Módulo 9: Pruebas y Buenas Prácticas
- Pruebas Unitarias con Web Test Runner
- Accesibilidad en Web Components
- Rendimiento y Optimización
- Patrones y Anti-patrones Comunes
