Decoradores en TypeScript

Avanzado
TypeScript
TypeScript
Actualizado: 04/05/2026

Diagrama: tutorial-typescript-decoradores

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, Reintentar envuelve el método original primero, y luego MedirTiempo envuelve 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 experimentalDecorators solo 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 - Autor del tutorial

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.