
Decoradores TC39 estándar
Los decoradores son funciones que modifican o extienden el comportamiento de clases y sus miembros de forma declarativa. TypeScript implementa la propuesta TC39 de decoradores en stage 3, que difiere significativamente de los decoradores experimentales anteriores (habilitados con experimentalDecorators).
Los decoradores estándar no requieren la flag experimentalDecorators en tsconfig.json. Se activan automáticamente cuando el target de compilación es compatible. La diferencia fundamental es que los decoradores estándar reciben dos argumentos: el valor decorado y un objeto de contexto que proporciona metadatos sobre la declaración.
function miDecorador(valorOriginal: Function, contexto: ClassMethodDecoratorContext) {
console.log(`Decorando método: ${String(contexto.name)}`)
return valorOriginal
}
class Ejemplo {
@miDecorador
saludar() {
return "Hola"
}
}
Los decoradores estándar TC39 y los experimentales (
experimentalDecorators) son incompatibles entre si. Un proyecto debe usar uno u otro, nunca ambos simultáneamente.
Los tipos de decoradores disponibles en el estándar son:
- Decoradores de clase: modifican la clase completa
- Decoradores de método: modifican métodos de instancia o estáticos
- Decoradores de accessor: modifican getters, setters o auto-accessors
- Decoradores de campo: modifican propiedades de clase
A diferencia de los experimentales, los decoradores estándar no soportan decoradores de parámetro ni emitDecoratorMetadata. Estas funcionalidades podrían llegar en propuestas futuras del TC39.
Decoradores de clase
Un decorador de clase recibe el constructor de la clase como primer argumento y un objeto ClassDecoratorContext como segundo. Puede devolver una nueva clase que reemplace la original o no devolver nada para dejar la clase intacta.
type Constructor = new (...args: any[]) => any
function Sellada(constructor: Constructor, contexto: ClassDecoratorContext) {
Object.seal(constructor)
Object.seal(constructor.prototype)
}
@Sellada
class Configuracion {
nombre: string
constructor(nombre: string) {
this.nombre = nombre
}
obtenerNombre() {
return this.nombre
}
}
El decorador Sellada invoca Object.seal sobre el constructor y su prototipo, impidiendo que se anadan o eliminen propiedades en tiempo de ejecución.
Reemplazar la clase original
Cuando un decorador de clase devuelve un valor, ese valor reemplaza la definición original de la clase:
function ConTimestamp<T extends Constructor>(
Base: T,
contexto: ClassDecoratorContext
) {
return class extends Base {
creadoEn = new Date()
constructor(...args: any[]) {
super(...args)
}
}
}
@ConTimestamp
class Documento {
titulo: string
constructor(titulo: string) {
this.titulo = titulo
}
}
const doc = new Documento("Informe")
console.log((doc as any).creadoEn)
Decorator factory de clase
Una decorator factory es una función que retorna el decorador real, permitiendo parametrización:
function Registrar(registro: Map<string, Constructor>) {
return function (constructor: Constructor, contexto: ClassDecoratorContext) {
registro.set(String(contexto.name), constructor)
}
}
const registroServicios = new Map<string, Constructor>()
@Registrar(registroServicios)
class ServicioUsuarios {
obtenerTodos() {
return ["Ana", "Luis"]
}
}
@Registrar(registroServicios)
class ServicioProductos {
obtenerTodos() {
return ["Laptop", "Monitor"]
}
}
console.log(registroServicios.size) // 2
console.log(registroServicios.has("ServicioUsuarios")) // true
Decoradores de método
Los decoradores de método reciben la función original como primer argumento y un ClassMethodDecoratorContext como segundo. Pueden devolver una función de reemplazo o no devolver nada.
function Log(
metodoOriginal: Function,
contexto: ClassMethodDecoratorContext
) {
const nombreMetodo = String(contexto.name)
function metodoReemplazado(this: any, ...args: any[]) {
console.log(`Llamando a ${nombreMetodo} con:`, args)
const resultado = metodoOriginal.call(this, ...args)
console.log(`${nombreMetodo} devolvio:`, resultado)
return resultado
}
return metodoReemplazado
}
class Calculadora {
@Log
sumar(a: number, b: number): number {
return a + b
}
@Log
multiplicar(a: number, b: number): number {
return a * b
}
}
const calc = new Calculadora()
calc.sumar(3, 5)
// Llamando a sumar con: [3, 5]
// sumar devolvio: 8
Memoización con decorador de método
Un caso práctico habitual es la memoización, que almacena resultados de llamadas anteriores para evitar computo repetido:
function Memoizar(
metodoOriginal: Function,
contexto: ClassMethodDecoratorContext
) {
const cache = new Map<string, unknown>()
function metodoMemoizado(this: any, ...args: any[]) {
const clave = JSON.stringify(args)
if (cache.has(clave)) {
console.log(`Cache hit para ${String(contexto.name)}(${clave})`)
return cache.get(clave)
}
const resultado = metodoOriginal.call(this, ...args)
cache.set(clave, resultado)
return resultado
}
return metodoMemoizado
}
class ServicioCalculo {
@Memoizar
fibonacci(n: number): number {
if (n <= 1) return n
return this.fibonacci(n - 1) + this.fibonacci(n - 2)
}
}
const servicio = new ServicioCalculo()
console.log(servicio.fibonacci(10)) // 55
console.log(servicio.fibonacci(10)) // Cache hit
Validación de argumentos
function ValidarPositivos(
metodoOriginal: Function,
contexto: ClassMethodDecoratorContext
) {
function metodoValidado(this: any, ...args: any[]) {
for (const arg of args) {
if (typeof arg === "number" && arg <= 0) {
throw new Error(
`${String(contexto.name)}: todos los argumentos numericos deben ser positivos`
)
}
}
return metodoOriginal.call(this, ...args)
}
return metodoValidado
}
class Geometria {
@ValidarPositivos
areaRectangulo(ancho: number, alto: number): number {
return ancho * alto
}
}
const geo = new Geometria()
console.log(geo.areaRectangulo(5, 3)) // 15
try {
geo.areaRectangulo(-2, 4)
} catch (error) {
console.error((error as Error).message)
// areaRectangulo: todos los argumentos numericos deben ser positivos
}
Decoradores de accessor y auto-accessors
Los auto-accessors son una característica del estándar TC39 que combina un campo privado con un getter y setter automáticos. Se declaran con la palabra clave accessor:
class Persona {
accessor nombre: string
constructor(nombre: string) {
this.nombre = nombre
}
}
const persona = new Persona("Ana")
console.log(persona.nombre) // Ana
persona.nombre = "Luis"
console.log(persona.nombre) // Luis
Un decorador de accessor recibe un objeto con las funciones get y set originales, y un ClassAccessorDecoratorContext. Debe devolver un objeto con las funciones get, set e init de reemplazo:
function Capitalizar(
target: ClassAccessorDecoratorTarget<any, string>,
contexto: ClassAccessorDecoratorContext<any, string>
) {
return {
get(this: any): string {
return target.get.call(this)
},
set(this: any, valor: string) {
target.set.call(this, valor.charAt(0).toUpperCase() + valor.slice(1))
},
init(valor: string): string {
return valor.charAt(0).toUpperCase() + valor.slice(1)
}
}
}
class Usuario {
@Capitalizar
accessor nombre: string
constructor(nombre: string) {
this.nombre = nombre
}
}
const usuario = new Usuario("carlos")
console.log(usuario.nombre) // Carlos
usuario.nombre = "maria"
console.log(usuario.nombre) // Maria
Decorador de getter y setter clásicos
Los decoradores también funcionan sobre getters y setters convencionales:
function Cachear(
metodoOriginal: Function,
contexto: ClassGetterDecoratorContext
) {
const cacheSymbol = Symbol("cache")
return function (this: any) {
if (this[cacheSymbol] === undefined) {
this[cacheSymbol] = metodoOriginal.call(this)
}
return this[cacheSymbol]
}
}
class Configuracion {
private datos = { tema: "oscuro", idioma: "es" }
@Cachear
get resumen(): string {
console.log("Calculando resumen...")
return JSON.stringify(this.datos)
}
}
const config = new Configuracion()
console.log(config.resumen) // Calculando resumen... {"tema":"oscuro","idioma":"es"}
console.log(config.resumen) // Usa cache, no recalcula
Decoradores de campo
Los decoradores de campo reciben undefined como primer argumento (el campo aun no tiene valor) y un ClassFieldDecoratorContext. Devuelven una función de inicialización que transforma el valor inicial:
function PorDefecto<T>(valorDefecto: T) {
return function (
_valor: undefined,
contexto: ClassFieldDecoratorContext
) {
return function (valorInicial: T): T {
return valorInicial ?? valorDefecto
}
}
}
class Opciones {
@PorDefecto(3000)
puerto!: number
@PorDefecto("localhost")
host!: string
@PorDefecto(true)
activo!: boolean
}
const opciones = new Opciones()
console.log(opciones.puerto) // 3000
console.log(opciones.host) // localhost
console.log(opciones.activo) // true
Validación en inicialización
function Rango(min: number, max: number) {
return function (
_valor: undefined,
contexto: ClassFieldDecoratorContext
) {
return function (valorInicial: number): number {
if (valorInicial < min || valorInicial > max) {
throw new Error(
`${String(contexto.name)} debe estar entre ${min} y ${max}`
)
}
return valorInicial
}
}
}
class ConfiguracionServidor {
@Rango(1, 65535)
puerto: number = 8080
@Rango(1, 100)
maxConexiones: number = 50
}
const servidor = new ConfiguracionServidor()
console.log(servidor.puerto) // 8080
console.log(servidor.maxConexiones) // 50
Decorator factories y composición
Las decorator factories permiten crear familias de decoradores configurables. La composición de múltiples decoradores sigue el orden de evaluación exterior-a-interior y aplicación interior-a-exterior:
function MedirTiempo(
metodoOriginal: Function,
contexto: ClassMethodDecoratorContext
) {
const nombre = String(contexto.name)
return function (this: any, ...args: any[]) {
const inicio = performance.now()
const resultado = metodoOriginal.call(this, ...args)
const duracion = performance.now() - inicio
console.log(`${nombre} tardo ${duracion.toFixed(2)}ms`)
return resultado
}
}
function Reintentar(intentos: number) {
return function (
metodoOriginal: Function,
contexto: ClassMethodDecoratorContext
) {
const nombre = String(contexto.name)
return function (this: any, ...args: any[]) {
let ultimoError: unknown
for (let i = 0; i < intentos; i++) {
try {
return metodoOriginal.call(this, ...args)
} catch (error) {
ultimoError = error
console.log(`${nombre}: intento ${i + 1} fallido`)
}
}
throw ultimoError
}
}
}
class ClienteAPI {
private contadorFallos = 0
@MedirTiempo
@Reintentar(3)
obtenerDatos(): string {
this.contadorFallos++
if (this.contadorFallos < 3) {
throw new Error("Fallo de conexión")
}
return "Datos obtenidos"
}
}
const cliente = new ClienteAPI()
try {
const resultado = cliente.obtenerDatos()
console.log(resultado)
} catch (error) {
console.error((error as Error).message)
}
Cuando se apilan múltiples decoradores, se evalúan de arriba hacia abajo (las factories se ejecutan en ese orden), pero se aplican de abajo hacia arriba. En el ejemplo,
Reintentarenvuelve el método original primero, y luegoMedirTiempoenvuelve el resultado.
El objeto de contexto
El objeto de contexto proporciona información valiosa sobre la declaración decorada:
function Inspeccionar(
metodoOriginal: Function,
contexto: ClassMethodDecoratorContext
) {
console.log("Nombre:", contexto.name)
console.log("Tipo:", contexto.kind) // "method", "getter", "setter", etc.
console.log("Es estático:", contexto.static)
console.log("Es privado:", contexto.private)
return metodoOriginal
}
class Servicio {
@Inspeccionar
procesar() {
return "procesado"
}
@Inspeccionar
static crear() {
return new Servicio()
}
}
El método addInitializer del contexto permite ejecutar lógica durante la inicialización de la clase o la instancia:
function AutoBind(
metodoOriginal: Function,
contexto: ClassMethodDecoratorContext
) {
const nombreMetodo = contexto.name
contexto.addInitializer(function (this: any) {
this[nombreMetodo] = this[nombreMetodo].bind(this)
})
return metodoOriginal
}
class Boton {
texto = "Enviar"
@AutoBind
manejarClick() {
console.log(`Click en: ${this.texto}`)
}
}
const boton = new Boton()
const handler = boton.manejarClick
handler() // Click en: Enviar (this se mantiene vinculado)
Decoradores legacy vs estándar
Los decoradores experimentales (experimentalDecorators: true en tsconfig.json) llevan disponibles en TypeScript desde versiones tempranas. Los decoradores estándar TC39 (stage 3) son la evolución oficial y el futuro de esta funcionalidad.
Diferencias principales:
| Caracteristica | Experimental (legacy) | Estandar TC39 |
|---|---|---|
| Activación | experimentalDecorators: true | Sin flag, activo por defecto |
| Firma de método | (target, key, descriptor) | (value, context) |
| Decoradores de parámetro | Si | No |
| emitDecoratorMetadata | Si | No |
| Auto-accessors | No | Si |
| Objeto de contexto | No | Si (name, kind, static, private, addInitializer) |
// Legacy (experimentalDecorators)
function LogLegacy(target: any, key: string, descriptor: PropertyDescriptor) {
const original = descriptor.value
descriptor.value = function (...args: any[]) {
console.log(`Llamando ${key}`)
return original.apply(this, args)
}
}
// Estandar TC39
function LogEstandar(
metodoOriginal: Function,
contexto: ClassMethodDecoratorContext
) {
return function (this: any, ...args: any[]) {
console.log(`Llamando ${String(contexto.name)}`)
return metodoOriginal.call(this, ...args)
}
}
Los decoradores estándar TC39 son la dirección adoptada por el comite de estandarización de JavaScript. Los proyectos nuevos deben preferir el estándar, reservando
experimentalDecoratorssolo para compatibilidad con frameworks que aun lo requieran.
Los frameworks y bibliotecas están migrando progresivamente al estándar. Mientras tanto, es importante verificar que versión de decoradores requiere cada dependencia del proyecto para evitar conflictos de compilación.
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 los decoradores TC39 estándar (stage 3) y sus diferencias con los experimentales. Implementar decoradores de clase, método, accessor y campo. Crear decorator factories parametrizadas. Aplicar patrones prácticos como logging, validación y memoización.