
Decoradores legacy vs decoradores estándar
TypeScript ha soportado decoradores durante años mediante la opción experimentalDecorators: true en tsconfig.json. Sin embargo, esta implementación divergía de la propuesta oficial de ECMAScript, creando incompatibilidades con el estándar.
TypeScript 5.0 implementó la propuesta TC39 Stage 3 de decoradores, que ya forma parte del estándar JavaScript. Esta nueva API es más segura, más expresiva y no requiere activar ninguna opción experimental.
Las diferencias principales:
| Aspecto | Legacy (experimentalDecorators) | Estándar TC39 (TypeScript 5+) |
|---|---|---|
| Configuración | experimentalDecorators: true | No requiere configuración |
| Parámetros | target, key, descriptor | value, context |
| emitDecoratorMetadata | Necesario para reflexión | No aplica |
| Compatibilidad ECMAScript | No estándar | Estándar TC39 Stage 3 |
Decoradores de clase
Con la nueva API, un decorador de clase recibe la clase y un objeto ClassDecoratorContext que proporciona información sobre la clase decorada:
function registrar<T extends abstract new (...args: any[]) => any>(
clase: T,
contexto: ClassDecoratorContext<T>
): T | void {
console.log(`Clase registrada: ${contexto.name}`);
// Puedes devolver una nueva clase o undefined para no modificarla
}
@registrar
class Servicio {
nombre = "mi servicio";
}
// Salida: "Clase registrada: Servicio"
Para reemplazar la clase con una versión extendida:
function sellada<T extends new (...args: any[]) => any>(
clase: T,
contexto: ClassDecoratorContext<T>
): T {
return class extends clase {
constructor(...args: any[]) {
super(...args);
Object.seal(this);
}
} as T;
}
@sellada
class Configuracion {
host = "localhost";
puerto = 3000;
}
const config = new Configuracion();
config.host = "api.ejemplo.com"; // ✓ permitido (propiedad existente)
(config as any).nueva = "valor"; // No añade propiedades (sellado)
Decoradores de método
Los decoradores de método reciben la función original y un ClassMethodDecoratorContext. Pueden devolver una nueva función que reemplaza el método original:
function registrarLlamada<T extends (...args: any[]) => any>(
metodo: T,
contexto: ClassMethodDecoratorContext
): T {
return function (this: any, ...args: any[]) {
console.log(`Llamando a ${String(contexto.name)} con args:`, args);
const resultado = metodo.apply(this, args);
console.log(`${String(contexto.name)} devolvió:`, resultado);
return resultado;
} as T;
}
class Calculadora {
@registrarLlamada
sumar(a: number, b: number): number {
return a + b;
}
}
const calc = new Calculadora();
calc.sumar(3, 4);
// "Llamando a sumar con args: [3, 4]"
// "sumar devolvió: 7"
Decoradores con addInitializer
El contexto de los decoradores expone addInitializer, que permite añadir código que se ejecuta después de que la clase es inicializada:
const registro = new Map<string, any[]>();
function registrarInstancias<T extends new (...args: any[]) => any>(
clase: T,
contexto: ClassDecoratorContext<T>
): void {
contexto.addInitializer(function (this: InstanceType<T>) {
const lista = registro.get(contexto.name as string) ?? [];
lista.push(this);
registro.set(contexto.name as string, lista);
});
}
@registrarInstancias
class Usuario {
constructor(public nombre: string) {}
}
new Usuario("Ana");
new Usuario("Luis");
console.log(registro.get("Usuario")?.length); // 2
Decorator factories (decoradores parametrizados)
Un decorator factory es una función que devuelve un decorador, permitiendo parametrizar su comportamiento:
function reintentar(maxIntentos: number, retrasoMs: number) {
return function <T extends (...args: any[]) => Promise<any>>(
metodo: T,
contexto: ClassMethodDecoratorContext
): T {
return async function (this: any, ...args: any[]) {
let intentos = 0;
while (intentos < maxIntentos) {
try {
return await metodo.apply(this, args);
} catch (error) {
intentos++;
if (intentos === maxIntentos) throw error;
await new Promise(r => setTimeout(r, retrasoMs));
console.log(`Reintentando ${String(contexto.name)} (intento ${intentos})`);
}
}
} as T;
};
}
class ApiCliente {
@reintentar(3, 1000)
async obtenerUsuarios(): Promise<unknown[]> {
// Llamada HTTP que puede fallar
const response = await fetch("/api/usuarios");
return response.json();
}
}
Decoradores de campo (field decorators)
Los decoradores de campo funcionan diferente porque los campos se inicializan por cada instancia, no en el prototipo. La nueva API los maneja mediante ClassFieldDecoratorContext:
function validarPositivo<T extends number>(
_target: undefined,
contexto: ClassFieldDecoratorContext
) {
return function (this: any, valor: T): T {
if (valor < 0) {
throw new Error(`${String(contexto.name)} no puede ser negativo`);
}
return valor;
};
}
class Producto {
@validarPositivo
precio: number = 0;
@validarPositivo
stock: number = 0;
constructor(precio: number, stock: number) {
this.precio = precio;
this.stock = stock;
}
}
new Producto(29.99, 10); // ✓
new Producto(-5, 10); // ✗ Error: precio no puede ser negativo
Decoradores de accesor (accessor keyword)
TypeScript 5.0 también introdujo la palabra clave accessor que combina getter y setter en una sola declaración, compatible con los decoradores estándar:
function capitalizarTexto(_: unknown, contexto: ClassAccessorDecoratorContext) {
return {
get(this: any): string {
return contexto.access.get(this);
},
set(this: any, valor: string): void {
contexto.access.set(this, valor.toUpperCase());
}
};
}
class Persona {
@capitalizarTexto
accessor nombre: string = "";
}
const p = new Persona();
p.nombre = "ana garcía";
console.log(p.nombre); // "ANA GARCÍA"
Caso de uso real: caché de métodos
Un decorador práctico para hacer caché de los resultados de métodos costosos:
function cache<T extends (...args: any[]) => any>(
metodo: T,
contexto: ClassMethodDecoratorContext
): T {
const almacen = new Map<string, ReturnType<T>>();
return function (this: any, ...args: any[]) {
const clave = JSON.stringify(args);
if (almacen.has(clave)) {
console.log(`[cache] ${String(contexto.name)} desde caché`);
return almacen.get(clave)!;
}
const resultado = metodo.apply(this, args);
almacen.set(clave, resultado);
return resultado;
} as T;
}
class Calculadora {
@cache
fibonacci(n: number): number {
if (n <= 1) return n;
return this.fibonacci(n - 1) + this.fibonacci(n - 2);
}
}
Migración desde decoradores legacy
Si tienes código con experimentalDecorators: true, la migración a los decoradores estándar requiere actualizar la firma de cada decorador. En muchos casos puedes mantener ambas versiones en coexistencia durante la transición.
Para proyectos nuevos, la recomendación es usar siempre los decoradores estándar TC39 ya que serán compatibles con el futuro de JavaScript.
Fuentes y referencias
Documentación oficial y recursos externos para profundizar en TypeScript
Documentación oficial de TypeScript
Alan Sastre
Ingeniero de Software y formador, CEO en CertiDevs
Ingeniero de software especializado en Full Stack y en Inteligencia Artificial. Como CEO de CertiDevs, TypeScript es una de sus áreas de expertise. Con más de 15 años programando, 6K seguidores en LinkedIn y experiencia como formador, Alan se dedica a crear contenido educativo de calidad para desarrolladores de todos los niveles.
Más tutoriales de TypeScript
Explora más contenido relacionado con TypeScript y continúa aprendiendo con nuestros tutoriales gratuitos.
Aprendizajes de esta lección
- Comprender las diferencias entre los decoradores legacy (experimentalDecorators) y los estándar TC39
- Implementar decoradores de clase, método, getter/setter, campo y accesor con la nueva API
- Usar el objeto ClassDecoratorContext y similares para acceder a metadatos en tiempo de ejecución
- Crear decoradores parametrizados (decorator factories) con la API estándar
- Aplicar decoradores en casos reales como logging, validación y caché