Decoradores JavaScript: qué son, cuándo llegan y cómo empezar a usarlos

Un decorador es una función que envuelve o modifica una clase, un método, una propiedad o un accesor. La sintaxis es sencilla: se pone @nombreDelDecorador justo antes de lo que quieres decorar. Nada más.

No son magia. Son funciones normales que reciben el elemento a decorar junto con un objeto de contexto, hacen algo con él y devuelven la versión modificada. El motor de JavaScript se encarga del resto.

@sealed
class MiClase {
  // ...
}

Si llevas tiempo con TypeScript o con Angular, los habrás visto. Pero hay un detalle importante: lo que has usado hasta ahora probablemente no es lo mismo que la propuesta oficial TC39 que está a punto de llegar al estándar.

La propuesta antigua y la nueva TC39 no son iguales

Aquí se confunde mucha gente. Los decoradores de TypeScript con experimentalDecorators: true en el tsconfig.json son una implementación antigua y no estándar. Llevan años funcionando y Angular los usa a fondo, pero nunca formaron parte del estándar de JavaScript.

La propuesta TC39 es diferente en su semántica. La clave está en cómo se llama al decorador:

  • La versión antigua recibe (target, key, descriptor)
  • La nueva TC39 recibe (value, context), donde context es un objeto con metadatos sobre el elemento decorado

TypeScript 5.0 soporta las dos formas. Con "experimentalDecorators": false (que es el valor por defecto desde TS 5.0) usas el nuevo estándar. Con true, el comportamiento antiguo. No mezcles los dos en el mismo proyecto.

Si quieres entender bien qué cambia en TypeScript cuando introduces estas features, te viene bien leer sobre TypeScript y los decoradores experimentales anteriores.

Decoradores de clase

Un decorador de clase recibe la propia clase como valor y puede devolver una versión modificada de ella. El caso más clásico es @sealed, que llama a Object.seal() sobre la clase para evitar que se añadan propiedades nuevas:

function sealed(value, context) {
  if (context.kind === 'class') {
    Object.seal(value);
    Object.seal(value.prototype);
  }
  return value;
}

@sealed
class Configuracion {
  host = 'localhost';
  puerto = 3000;
}

A partir de aquí, intentar añadir una propiedad nueva a Configuracion en tiempo de ejecución lanzará un error en modo estricto o simplemente se ignorará.

Decoradores de método

Son los más útiles en el día a día. Puedes usarlos para añadir logging, memoización o reintentos automáticos sin tocar el cuerpo del método original.

Ejemplo: logging automático

function log(value, context) {
  return function (...args) {
    console.log(`[${context.name}] entrada:`, args);
    const resultado = value.apply(this, args);
    console.log(`[${context.name}] salida:`, resultado);
    return resultado;
  };
}

class Calculadora {
  @log
  sumar(a, b) {
    return a + b;
  }
}

const calc = new Calculadora();
calc.sumar(2, 3);
// [sumar] entrada: [2, 3]
// [sumar] salida: 5

Ejemplo: memoización por argumentos

function memoize(value, context) {
  const cache = new Map();
  return function (...args) {
    const key = JSON.stringify(args);
    if (cache.has(key)) return cache.get(key);
    const resultado = value.apply(this, args);
    cache.set(key, resultado);
    return resultado;
  };
}

class MatrixUtils {
  @memoize
  fibonacci(n) {
    if (n <= 1) return n;
    return this.fibonacci(n - 1) + this.fibonacci(n - 2);
  }
}

La segunda vez que llamas a fibonacci(10) no recalcula nada. El decorador se ocupa de gestionar la caché de forma transparente.

Decoradores de campo (class fields)

Los decoradores de campo funcionan sobre las propiedades de una clase. Son útiles para añadir restricciones sin tener que convertir el campo en un getter/setter a mano.

function readonly(value, context) {
  return {
    get() {
      return value;
    },
    set() {
      throw new Error(`El campo ${context.name} es de solo lectura.`);
    }
  };
}

class Configuracion {
  @readonly
  VERSION = '1.0.0';
}

const cfg = new Configuracion();
console.log(cfg.VERSION); // '1.0.0'
cfg.VERSION = '2.0.0';    // Error: El campo VERSION es de solo lectura.

La diferencia con Object.freeze() es que freeze bloquea el objeto entero. Aquí controlas campo por campo, lo que te da más flexibilidad.

Decoradores de accesor

La propuesta TC39 introduce también una nueva palabra clave: accessor. Usada con un campo, genera automáticamente un getter y un setter internos. Esto es especialmente útil cuando el decorador necesita interceptar las asignaciones.

function rangeCheck(min, max) {
  return function (value, context) {
    return {
      get() {
        return value.call(this);
      },
      set(nuevoValor) {
        if (nuevoValor < min || nuevoValor > max) {
          throw new RangeError(
            `${context.name} debe estar entre ${min} y ${max}`
          );
        }
        value.call(this, nuevoValor);
      }
    };
  };
}

class Jugador {
  @rangeCheck(0, 100)
  accessor vida = 100;
}

const j = new Jugador();
j.vida = 80;   // OK
j.vida = 150;  // RangeError: vida debe estar entre 0 y 100

Sin accessor, tendrías que escribir el getter y el setter a mano en la clase. El decorador ya no puede hacerlo por ti porque no tiene dónde engancharse.

Dónde puedes usarlos hoy

En junio de 2026 la situación es esta:

  • Bun 1.3.10 o superior: soporte nativo del estándar TC39. No necesitas nada más.
  • Chrome 130+: disponible con o sin flag según la versión exacta del navegador.
  • TypeScript 5.x: con "experimentalDecorators": false en el tsconfig.json, que es el valor por defecto.
  • Babel: con el plugin @babel/plugin-proposal-decorators y version: "2023-11" en la configuración.
  • Node.js: aún sin soporte nativo. Necesitas pasar por TypeScript o Babel.

Para Babel, el archivo de configuración queda así:

{
  "plugins": [
    ["@babel/plugin-proposal-decorators", { "version": "2023-11" }]
  ]
}

Si trabajas con Node.js y no quieres transpilar, por ahora no hay alternativa. Es el entorno que va más rezagado.

El caso de uso más claro: ORMs y frameworks

Los decoradores brillan cuando defines entidades de base de datos. TypeORM y MikroORM ya tienen versiones compatibles con el nuevo estándar, y el resultado es muy legible:

import { Entity, PrimaryKey, Property } from '@mikro-orm/core';

@Entity()
class Producto {
  @PrimaryKey()
  id: number;

  @Property()
  nombre: string;

  @Property()
  precio: number;
}

NestJS está en proceso de migración desde la implementación experimental a la nueva. Si tienes un proyecto NestJS, conviene que revises el estado de compatibilidad antes de actualizar TypeScript.

Para entender qué implica esto dentro del contexto más amplio del lenguaje, echa un vistazo a JavaScript moderno: base del lenguaje.

Por qué vale la pena aprenderlos ahora

La propuesta lleva años dando vueltas en TC39 y ha cambiado varias veces. Eso ha generado confusión y ha hecho que mucha gente la ignorase. Pero con Stage 2.7 ya superado y soporte en Bun y Chrome, el estándar está prácticamente cerrado.

Si usas TypeScript habitualmente, los decoradores TC39 son compatibles desde la versión 5.0. No tienes que esperar a que lleguen a Node.js para empezar a escribirlos y entender cómo funcionan. Y cuando lleguen, el código que ya tengas seguirá funcionando sin tocar nada.

Imagen: Pexels / anshul kumar

COMPARTE ESTE ARTÍCULO

COMPARTIR EN FACEBOOK
COMPARTIR EN TWITTER
COMPARTIR EN LINKEDIN
COMPARTIR EN WHATSAPP