Los decorators de TypeScript permiten adjuntar comportamiento a clases, métodos, accesores, propiedades y parámetros sin modificar el código original directamente. Son más visibles en Angular, pero el patrón es útil en cualquier lugar donde quieras mantener las preocupaciones transversales —como logging, validación o metadatos— fuera de tu lógica de negocio central. Este post cubre cómo funcionan los decorators, los cinco tipos que soporta TypeScript y cómo construir el tuyo propio.

Entendiendo los Fundamentos de los Decorators de TypeScript

Un decorator es una función. Cuando pones @Something encima de una clase o método, TypeScript llama a esa función con metadatos sobre el objetivo: el constructor, el descriptor del método, el nombre de la propiedad, etc. El decorator puede encapsular comportamiento, adjuntar nuevas propiedades o registrar metadatos para uso posterior.

Por Qué Importan los Decorators

Los decorators resuelven un problema específico: las preocupaciones transversales se acumulan en los métodos con el tiempo. El logging, las métricas, la validación y las verificaciones de permisos terminan dispersos dentro de funciones que deberían ocuparse de una sola cosa. Los decorators permiten extraer esa lógica y reutilizarla en toda la base de código con una única anotación.

Una vez que hayas escrito un decorator de logging, puedes aplicarlo a cien métodos sin tocar ninguno de ellos. Cuando el comportamiento del logging necesite cambiar, lo cambias en un solo lugar. Ese es el valor real, no la sintaxis.

Angular utiliza decorators extensivamente para @Component, @Injectable, @Input y marcadores similares. Entender lo que ocurre bajo el capó ayuda cuando necesitas depurar, construir extensiones de biblioteca personalizadas o escribir código agnóstico al framework.

Tipos de Decorators en TypeScript

TypeScript soporta cinco tipos de decorators, cada uno dirigido a un constructo de lenguaje diferente.

Los decorators de clase reciben un constructor como argumento. Pueden encapsular el constructor, adjuntar nuevas propiedades al prototipo o reemplazar la clase por completo. El uso más común es marcar clases con roles o ejecutar lógica de inicialización.

Los decorators de método reciben el prototipo de la clase, el nombre del método y el property descriptor. Reemplazas descriptor.value con una función wrapper. Es el tipo más utilizado, adecuado para logging, caching y control de acceso.

Los decorators de accesor funcionan igual que los decorators de método pero apuntan a getters y setters. Útiles para validación o transformación en el acceso a propiedades, aunque menos necesarios que los decorators de método.

Los decorators de propiedad reciben el objetivo y el nombre de la propiedad. No pueden modificar inicializadores de propiedades directamente, pero pueden almacenar metadatos via reflect-metadata. Los ORMs usan este patrón extensivamente para mapear propiedades de clase a columnas de base de datos.

Los decorators de parámetro anotan parámetros de métodos con su índice en la firma. Los frameworks de inyección de dependencias usan esto para saber qué argumentos inyectar automáticamente.

Prerrequisitos para Usar Decorators

Habilita dos opciones del compilador en tu tsconfig.json:

{
  "compilerOptions": {
    "target": "ES6",
    "experimentalDecorators": true,
    "emitDecoratorMetadata": true,
    ...
  },
  ...
}

experimentalDecorators habilita la sintaxis de decorators. emitDecoratorMetadata emite información de tipos en tiempo de ejecución, que la librería reflect-metadata usa para escenarios avanzados de reflexión como la inyección de dependencias.

Cómo Usar Decorators en la Práctica

Ejemplo de Decorator de Clase

Aquí hay un decorator de clase simple que agrega una propiedad de timestamp para rastrear los tiempos de instanciación:

function Timestamped(constructor: Function) {
  constructor.prototype.createdAt = new Date();
}

@Timestamped
class ReportService {
  constructor() {
    // Some initialization code
  }

  generateReport() {
    console.log("Report generated");
  }
}

const service = new ReportService();
console.log(service["createdAt"]); // A Date object representing creation time

Timestamped modifica el prototipo del constructor, por lo que cada instancia de ReportService obtiene una propiedad createdAt establecida al momento en que la clase fue decorada por primera vez. Ten en cuenta que esto se ejecuta una vez en el momento de la definición de la clase, no en cada instanciación, lo cual es un error común para los que empiezan.

Ejemplo de Decorator de Método

Los decorators de método permiten interceptar y ampliar llamadas a funciones. Aquí hay un decorator de logging simple:

function LogMethod(
  target: any,
  propertyName: string,
  descriptor: PropertyDescriptor
) {
  const originalMethod = descriptor.value;
  
  descriptor.value = function (...args: any[]) {
    console.log(`Calling ${propertyName} with arguments:`, args);
    const result = originalMethod.apply(this, args);
    console.log(`Result of ${propertyName}:`, result);
    return result;
  };

  return descriptor;
}

class TaskService {
  @LogMethod
  runTask(taskId: string) {
    return `Task ${taskId} completed successfully!`;
  }
}

const taskService = new TaskService();
taskService.runTask("1234");

LogMethod reemplaza descriptor.value con un wrapper que registra logs antes y después de la llamada original. El método original se ejecuta via apply(this, args) para preservar el binding correcto de this.

Ejemplo de Decorator de Propiedad

Los decorators de propiedad pueden adjuntar metadatos a propiedades específicas de una clase. Por ejemplo, imagina un ORM que necesita identificar qué propiedades mapean a columnas de base de datos:

import "reflect-metadata";

const COLUMN_KEY = Symbol("column");

/**
 * Attach metadata to a property to mark it as a DB column.
 */
function Column(columnName?: string): PropertyDecorator {
  return (target, propertyKey) => {
    Reflect.defineMetadata(COLUMN_KEY, columnName || propertyKey, target, propertyKey);
  };
}

class User {
  @Column("user_id")
  id: number;

  @Column()
  username: string;

  @Column()
  email: string;
}

// Later you can retrieve metadata to build queries or define migrations
function getColumnMetadata(target: any, propertyKey: string) {
  return Reflect.getMetadata(COLUMN_KEY, target, propertyKey);
}

Cada propiedad decorada almacena metadatos sobre el nombre de su columna. La capa del ORM lee esos metadatos en tiempo de ejecución para construir queries, sin que ninguna de esta lógica resida dentro de la clase User en sí misma.

Cómo Crear Tus Propios Decorators

Construir un decorator personalizado significa escribir una función que coincida con la firma esperada para el tipo de objetivo. El ejemplo de validador a continuación combina un decorator de parámetro y un decorator de método para garantizar argumentos no vacíos.

Ejemplo Paso a Paso: Decorator de Validación Personalizado

  1. Crea la función decorator:
import "reflect-metadata";

const VALIDATE_KEY = Symbol("validateParams");

function NotEmpty(
  target: Object,
  propertyKey: string | symbol,
  parameterIndex: number
) {
  let existingRequiredParameters: number[] =
    Reflect.getOwnMetadata(VALIDATE_KEY, target, propertyKey) || [];
  existingRequiredParameters.push(parameterIndex);
  Reflect.defineMetadata(
    VALIDATE_KEY,
    existingRequiredParameters,
    target,
    propertyKey
  );
}
  1. Crea una función auxiliar para ejecutar las validaciones antes de llamar al método original:
function Validate(
  target: any,
  propertyName: string,
  descriptor: PropertyDescriptor
) {
  const method = descriptor.value;

  descriptor.value = function (...args: any[]) {
    const requiredParameters: number[] =
      Reflect.getOwnMetadata(VALIDATE_KEY, target, propertyName) || [];

    requiredParameters.forEach((index) => {
      if (args[index] === null || args[index] === undefined || args[index] === "") {
        throw new Error(`Validation Error: Parameter at index ${index} is empty`);
      }
    });

    return method.apply(this, args);
  };

  return descriptor;
}
  1. Aplica estos decorators en una clase:
class MailService {
  @Validate
  sendEmail(@NotEmpty recipient: string, message: string) {
    console.log(`Sending email to ${recipient} with message: ${message}`);
  }
}

const mailService = new MailService();
mailService.sendEmail("john@example.com", "Hello John!"); // Works fine
mailService.sendEmail("", "This should fail validation"); // Throws Validation Error

@NotEmpty registra qué índices de parámetros requieren validación. @Validate lee esos índices antes de invocar el método original y lanza un error si alguno de ellos está vacío. La lógica de validación reside completamente en los decorators, no en sendEmail.

Decorators en Angular

Angular usa decorators de clase para marcar componentes, servicios, directivas y pipes. Cuando escribes @Component(...), el compilador de Angular lee los metadatos de ese decorator para entender cómo compilar y conectar la clase. @Injectable() le dice al sistema de inyección de dependencias que la clase puede ser instanciada e inyectada en otros lugares.

No necesitas entender los internos para usar Angular, pero saber cómo funcionan los decorators hace que la depuración sea mucho menos misteriosa cuando los metadatos faltan o están mal configurados.

Buenas Prácticas para Decorators

Mantén cada decorator enfocado en una sola responsabilidad. Un decorator que hace logging, valida y transforma datos es más difícil de depurar que tres separados. Cuando los decorators modifican prototipos o descriptors, documenta lo que cambian para que los lectores futuros no tengan que hacer ingeniería inversa. La librería reflect-metadata es poderosa pero puede volverse difícil de razonar si se usa en exceso. Si adjuntas metadatos para un solo caso de uso, considera si un enfoque más simple funcionaría.

Prueba tus decorators explícitamente. Dado que aplican comportamiento transversal, es fácil pasarlos por alto en los tests unitarios. Escribe tests que verifiquen el comportamiento del decorator de forma aislada, no solo el método que encapsula.

Avanzando con los Decorators de TypeScript

El patrón escala bien. Los decorator factories permiten pasar argumentos de configuración a un decorator. Apilar múltiples decorators en un único método es sencillo, aunque el orden de ejecución (de abajo hacia arriba) confunde a la gente al principio. Para proyectos más grandes, los decorators se combinan bien con patrones de inyección de dependencias y middleware.

Lo principal que hay que interiorizar es que un decorator es solo una función. Una vez que eso queda claro, el resto es mecánica.