TypeScript en Producción: De Principiante a Experto
Guía completa para dominar TypeScript en proyectos empresariales y reducir bugs en 80%.
Durante años, la reutilización de componentes ha requerido frameworks como React, Vue o Angular. Pero Web Components ofrece una alternativa nativa del navegador que cambia el juego completamente.
Los Web Components son un conjunto de APIs estándar de HTML5 que permite crear componentes personalizados, reutilizables y encapsulados:
<!-- Sin Web Components -->
<script src="react.min.js"></script>
<script src="react-dom.min.js"></script>
<script src="button-component.js"></script>
<!-- 3+ archivos, cientos de KB -->
<!-- Con Web Components -->
<script src="button-component.js"></script>
<!-- 1 archivo, típicamente <5KB -->
<!-- El mismo componente funciona en cualquier lugar -->
<my-button theme="primary">Click aquí</my-button>
<!-- En HTML puro -->
<!DOCTYPE html>
<my-button>Botón</my-button>
<!-- En React -->
<MyButton>Botón</MyButton> {/* ¡También funciona! */}
<!-- En Vue -->
<my-button>Botón</my-button>
<!-- En Angular -->
<my-button>Botón</my-button>
<!-- Los estilos dentro del componente NO afectan al exterior -->
<style>
button { color: red; }
</style>
<my-button>
<!-- Internamente, los botones permanecen sin estilo -->
</my-button>
// button-component.js
class MyButton extends HTMLElement {
constructor() {
super();
// Crear Shadow DOM
this.attachShadow({ mode: 'open' });
}
connectedCallback() {
this.render();
}
render() {
this.shadowRoot.innerHTML = `
<style>
button {
padding: 10px 20px;
border: none;
border-radius: 4px;
background: #007bff;
color: white;
cursor: pointer;
font-size: 14px;
}
button:hover {
background: #0056b3;
}
</style>
<button>
<slot></slot>
</button>
`;
// Listeners
this.shadowRoot.querySelector('button')
.addEventListener('click', () => this.handleClick());
}
handleClick() {
// Emitir evento personalizado
this.dispatchEvent(new CustomEvent('button-clicked', {
detail: { message: this.textContent },
bubbles: true,
}));
}
}
// Registrar el elemento personalizado
customElements.define('my-button', MyButton);
<!DOCTYPE html>
<html>
<head>
<script src="button-component.js"></script>
</head>
<body>
<my-button>Haz clic</my-button>
<script>
document.querySelector('my-button')
.addEventListener('button-clicked', (e) => {
console.log('¡Botón clickeado!', e.detail.message);
});
</script>
</body>
</html>
class CardComponent extends HTMLElement {
// Atributos observados
static get observedAttributes() {
return ['title', 'subtitle', 'image'];
}
constructor() {
super();
this.attachShadow({ mode: 'open' });
}
connectedCallback() {
this.render();
}
attributeChangedCallback(name, oldValue, newValue) {
if (oldValue !== newValue) {
this.render();
}
}
render() {
const title = this.getAttribute('title') || 'Sin título';
const subtitle = this.getAttribute('subtitle') || '';
const image = this.getAttribute('image') || '';
this.shadowRoot.innerHTML = `
<style>
:host {
display: block;
--card-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
}
.card {
border-radius: 8px;
overflow: hidden;
box-shadow: var(--card-shadow);
background: white;
transition: transform 0.3s;
}
.card:hover {
transform: translateY(-4px);
}
.card-image {
width: 100%;
height: 200px;
object-fit: cover;
}
.card-content {
padding: 16px;
}
.card-title {
font-size: 18px;
font-weight: bold;
margin: 0 0 8px 0;
}
.card-subtitle {
color: #666;
font-size: 14px;
margin: 0;
}
</style>
<div class="card">
${image ? `<img src="${image}" alt="${title}" class="card-image">` : ''}
<div class="card-content">
<h3 class="card-title">${title}</h3>
${subtitle ? `<p class="card-subtitle">${subtitle}</p>` : ''}
<slot></slot>
</div>
</div>
`;
}
}
customElements.define('card-component', CardComponent);
<card-component
title="Producto Destacado"
subtitle="Calidad premium"
image="https://ejemplo.com/imagen.jpg">
<p>Este es un producto increíble.</p>
</card-component>
<!-- Con CSS personalizado -->
<style>
card-component {
--card-shadow: 0 4px 12px rgba(0, 0, 0, 0.2);
}
</style>
// emit-component.js
class EmitterComponent extends HTMLElement {
connectedCallback() {
this.shadowRoot.innerHTML = `
<button>Enviar dato</button>
`;
this.shadowRoot.querySelector('button')
.addEventListener('click', () => {
this.emitData();
});
}
emitData() {
this.dispatchEvent(new CustomEvent('data-changed', {
detail: { value: 'Hola desde el componente' },
bubbles: true,
composed: true, // Importante: atraviesa Shadow DOM
}));
}
}
customElements.define('emitter-component', EmitterComponent);
// Uso: Comunicación bidireccional
document.querySelector('emitter-component')
.addEventListener('data-changed', (e) => {
console.log('Dato recibido:', e.detail.value);
// Hacer algo con el dato
});
| Ventaja | Impacto |
|---|---|
| Cero dependencias | Menor bundle size, menos vulnerabilidades |
| Reutilizable universalmente | Código compartible entre proyectos |
| Encapsulación real | Estilos no se filtran, arquitectura limpia |
| Estándar web | Soportado nativamente en navegadores modernos |
| Rendimiento | Direct DOM manipulation, sin virtual DOM overhead |
| Facilidad de aprendizaje | Solo JavaScript vanilla, HTML y CSS |
| Desventaja | Solución |
|---|---|
| Navegadores antiguos | Polyfills disponibles |
| Debugging complejo | DevTools mejoradas constantemente |
| Falta de ecosistema | Creciendo: Lit, Stencil, FAST |
| SSR limitado | Mejorado en navegadores recientes |
| Curva de aprendizaje | Documentación en mejora |
Para Web Components complejos, Lit simplifica el desarrollo:
import { LitElement, html, css } from 'lit';
class SmartButton extends LitElement {
static styles = css`
button {
padding: 10px 20px;
background: #007bff;
color: white;
border: none;
border-radius: 4px;
cursor: pointer;
}
`;
static properties = {
disabled: { type: Boolean },
label: { type: String },
};
constructor() {
super();
this.disabled = false;
this.label = 'Click me';
}
render() {
return html`
<button ?disabled=${this.disabled}>
${this.label}
</button>
`;
}
}
customElements.define('smart-button', SmartButton);
my-component-library/
├── src/
│ ├── button/
│ │ ├── button.js
│ │ └── button.css
│ ├── card/
│ │ ├── card.js
│ │ └── card.css
│ └── modal/
│ ├── modal.js
│ └── modal.css
├── package.json
└── README.md
{
"name": "@mycompany/components",
"version": "1.0.0",
"main": "dist/index.js",
"files": ["dist"],
"scripts": {
"build": "webpack",
"test": "vitest"
}
}
# En cualquier proyecto
npm install @mycompany/components
# O desde CDN
<script src="https://cdn.example.com/components.js"></script>
<!-- Funciona en React, Vue, Angular, o HTML puro -->
<my-button>Haz clic</my-button>
<my-card title="Producto"></my-card>
<my-modal id="login"></my-modal>
| Navegador | Soporte |
|---|---|
| Chrome | ✓ 67+ |
| Firefox | ✓ 63+ |
| Safari | ✓ 10.1+ |
| Edge | ✓ 79+ |
| IE 11 | ✗ (requiere polyfills) |
// Detectar soporte y cargar polyfills si es necesario
if (!window.customElements) {
const script = document.createElement('script');
script.src = 'https://cdn.jsdelivr.net/npm/@webcomponents/webcomponentsjs@2/webcomponents-loader.js';
document.head.appendChild(script);
}
Los Web Components representan el futuro de la reutilización de código en la web. No reemplazan a los frameworks, sino que ofrecen una alternativa más ligera y universal para casos específicos:
La era de los componentes sin framework ha llegado.
Próximos pasos:
Recursos:
Profundiza en temas de arquitectura y rendimiento.