tutoriales.com

Dominando los Decoradores en TypeScript: Una Guía Práctica con Ejemplos Reales

Este tutorial exhaustivo te sumergirá en el mundo de los decoradores en TypeScript, una característica poderosa que te permite modificar el comportamiento de clases, métodos, propiedades y parámetros en tiempo de diseño. Exploraremos los diferentes tipos de decoradores, su sintaxis, cómo crearlos desde cero y cómo aplicarlos en escenarios reales para mejorar la modularidad y la legibilidad de tu código.

Intermedio15 min de lectura11 views16 de marzo de 2026Reportar error

🚀 Introducción a los Decoradores en TypeScript

TypeScript es un superconjunto de JavaScript que añade tipado estático y otras características potentes para construir aplicaciones escalables. Entre estas características, los decoradores destacan como una forma elegante y declarativa de añadir metadatos y lógica a clases, métodos, propiedades y parámetros. Si bien son un concepto que ha estado presente en otros lenguajes (como las anotaciones en Java o los atributos en C#), en TypeScript ofrecen una flexibilidad impresionante, especialmente en frameworks como Angular o NestJS, donde son omnipresentes.

¿Qué son los Decoradores?

Un decorador es una función especial que puede ser adjuntada a una declaración de clase, método, accesor, propiedad o parámetro. Los decoradores se ejecutan en tiempo de diseño (cuando el código es compilado), no en tiempo de ejecución. Su propósito principal es modificar o extender el comportamiento de lo que están decorando sin alterar su implementación original. Esto permite una programación más declarativa y reduce el boilerplate.

💡 **Consejo:** Los decoradores son una propuesta de Stage 3 para ECMAScript, pero ya son una característica madura y ampliamente utilizada en TypeScript gracias a su soporte experimental. Asegúrate de habilitar la opción `experimentalDecorators` en tu `tsconfig.json`.

🛠️ Configuración Inicial: Habilitando Decoradores

Antes de empezar a usar decoradores, necesitamos asegurarnos de que TypeScript está configurado correctamente. Abre tu archivo tsconfig.json y busca la propiedad compilerOptions. Asegúrate de que experimentalDecorators y emitDecoratorMetadata (esta última es útil para frameworks que usan reflect-metadata, como Angular) estén establecidos en true.

{
  "compilerOptions": {
    "target": "es2016",
    "module": "commonjs",
    "outDir": "./dist",
    "esModuleInterop": true,
    "forceConsistentCasingInFileNames": true,
    "strict": true,
    "skipLibCheck": true,
    "experimentalDecorators": true, <mark>¡Asegúrate de que sea true!</mark>
    "emitDecoratorMetadata": true  <mark>¡También es importante!</mark>
  }
}

Una vez hecho esto, podemos empezar a explorar los diferentes tipos de decoradores.


✨ Tipos de Decoradores y Cómo Funcionan

TypeScript soporta cinco tipos de decoradores, cada uno aplicable a una parte diferente de una declaración.

1. Decoradores de Clases (Class Decorators)

Un decorador de clase se aplica directamente a la declaración de una clase. Recibe el constructor de la clase como único argumento y puede usarse para modificar el constructor de la clase o reemplazar la clase por una nueva. Son ideales para añadir lógica a la instanciación o registrar clases en un contenedor de inyección de dependencias.

Sintaxis:

@nombreDecorador(argumentos)
class MiClase {
  // ...
}

Ejemplo: Un decorador para añadir un metadato version a una clase.

function Versionable(version: string) {
  return function <T extends { new(...args: any[]): {} }>(constructor: T) {
    return class extends constructor {
      versionApp = version;

      constructor(...args: any[]) {
        super(...args);
        console.log(`Clase ${constructor.name} version ${version} inicializada.`);
      }
    };
  };
}

@Versionable("1.0.0")
class UserService {
  constructor(public name: string) {}

  getUserInfo() {
    return `Usuario: ${this.name}`;
  }
}

const user = new UserService("Alice");
console.log(user.getUserInfo());
// Propiedad 'versionApp' añadida dinámicamente
console.log((user as any).versionApp);

/*
Output:
Clase UserService version 1.0.0 inicializada.
Usuario: Alice
1.0.0
*/

En este ejemplo, Versionable es un factory de decoradores que toma un argumento version. Devuelve una función que es el decorador real. Este decorador recibe el constructor de UserService y devuelve una nueva clase que extiende la original, añadiéndole una propiedad versionApp y un mensaje en el constructor.

2. Decoradores de Métodos (Method Decorators)

Los decoradores de métodos se aplican a la declaración de un método. Reciben tres argumentos: el prototipo de la clase (para miembros estáticos, el constructor de la clase), el nombre del miembro y un descriptor de propiedad. Son útiles para modificar, reemplazar o envolver el método original.

Sintaxis:

class MiClase {
  @nombreDecorador(argumentos)
  miMetodo() {
    // ...
  }
}

Ejemplo: Un decorador para loguear el tiempo de ejecución de un método.

function LogExecutionTime(target: any, propertyKey: string, descriptor: PropertyDescriptor) {
  const originalMethod = descriptor.value;

  descriptor.value = function (...args: any[]) {
    const start = performance.now();
    const result = originalMethod.apply(this, args);
    const end = performance.now();
    console.log(`Método ${propertyKey} ejecutado en ${end - start} ms.`);
    return result;
  };

  return descriptor;
}

class DataProcessor {
  @LogExecutionTime
  processData(data: number[]): number {
    // Simula una operación que consume tiempo
    let sum = 0;
    for (let i = 0; i < 1_000_000; i++) {
      sum += data[i % data.length];
    }
    return sum;
  }
}

const processor = new DataProcessor();
processor.processData([1, 2, 3, 4, 5]);

/*
Output:
Método processData ejecutado en [unos pocos milisegundos] ms.
*/

Aquí, LogExecutionTime envuelve el método processData. Guarda una referencia al método original, luego reemplaza el descriptor value con una nueva función que registra el tiempo antes y después de llamar al método original.

3. Decoradores de Accesor (Accessor Decorators)

Se aplican a las declaraciones de getters y setters. Al igual que los decoradores de métodos, reciben el prototipo de la clase, el nombre del miembro y un descriptor de propiedad. Permiten modificar el comportamiento de los accesores.

Sintaxis:

class MiClase {
  @nombreDecorador(argumentos)
  get miPropiedad() {
    // ...
  }
}

Ejemplo: Un decorador para hacer un accesor de solo lectura después de la inicialización.

function ReadOnlyAfterInit(target: any, propertyKey: string, descriptor: PropertyDescriptor) {
  const originalSetter = descriptor.set;

  descriptor.set = function(value: any) {
    if (this._initialized) {
      console.warn(`Advertencia: Intento de modificar la propiedad '${propertyKey}' después de la inicialización.`);
      return;
    }
    originalSetter?.apply(this, [value]);
  };
  return descriptor;
}

class UserProfile {
  _initialized: boolean = false;
  private _email: string = '';

  constructor(email: string) {
    this._email = email;
    // Simula la inicialización de la instancia
    setTimeout(() => this._initialized = true, 100);
  }

  @ReadOnlyAfterInit
  get email(): string {
    return this._email;
  }

  set email(value: string) {
    this._email = value;
  }
}

const profile = new UserProfile("john.doe@example.com");
console.log(profile.email); // john.doe@example.com

profile.email = "jane.doe@example.com"; // Esto funcionará, _initialized es falso
console.log(profile.email); // jane.doe@example.com

setTimeout(() => {
  profile.email = "new.email@example.com"; // Esto generará una advertencia después de 100ms
  console.log(profile.email); // jane.doe@example.com (no se cambia)
}, 200);

4. Decoradores de Propiedades (Property Decorators)

Los decoradores de propiedades se aplican a la declaración de una propiedad. Reciben dos argumentos: el prototipo de la clase y el nombre de la propiedad. Son ideales para añadir metadatos a una propiedad o inicializar su valor de forma declarativa.

Sintaxis:

class MiClase {
  @nombreDecorador(argumentos)
  miPropiedad: tipo;
}

Ejemplo: Un decorador para validar que una propiedad no esté vacía.

const validators: { [key: string]: ((value: any) => boolean)[] } = {};

function Required(target: any, propertyKey: string) {
  validators[propertyKey] = validators[propertyKey] || [];
  validators[propertyKey].push((value: any) => value !== null && value !== undefined && value !== '');
}

function validate(obj: any): boolean {
  for (const propertyKey in validators) {
    if (validators.hasOwnProperty(propertyKey)) {
      for (const validatorFn of validators[propertyKey]) {
        if (!validatorFn(obj[propertyKey])) {
          console.error(`Error de validación: La propiedad '${propertyKey}' es requerida.`);
          return false;
        }
      }
    }
  }
  return true;
}

class Product {
  @Required
  name: string;

  price: number;

  constructor(name: string, price: number) {
    this.name = name;
    this.price = price;
  }
}

const product1 = new Product("Laptop", 1200);
console.log(`Producto 1 válido: ${validate(product1)}`); // Producto 1 válido: true

const product2 = new Product("", 50);
console.log(`Producto 2 válido: ${validate(product2)}`); // Error de validación: La propiedad 'name' es requerida. 
                                                     // Producto 2 válido: false

Este ejemplo muestra cómo los decoradores de propiedades pueden usarse para registrar validadores. El decorador Required no modifica la propiedad en sí, sino que añade una función de validación a un registro global validators. Luego, una función validate puede usar este registro para comprobar la validez de un objeto.

5. Decoradores de Parámetros (Parameter Decorators)

Los decoradores de parámetros se aplican a la declaración de un parámetro dentro de un constructor, método o accesor. Reciben tres argumentos: el prototipo de la clase, el nombre del miembro y el índice ordinal del parámetro en la lista de argumentos de la función. Son excelentes para registrar metadatos sobre los parámetros, lo cual es fundamental para la inyección de dependencias o la generación de documentación.

Sintaxis:

class MiClase {
  miMetodo(@nombreDecorador(argumentos) miParametro: tipo) {
    // ...
  }
}

Ejemplo: Un decorador para marcar un parámetro como inyectable.

const injectableParams: { [key: string]: number[] } = {};

function Inject(target: any, propertyKey: string | symbol | undefined, parameterIndex: number) {
  if (propertyKey) {
    // Decorador de parámetro en un método o accesor
    injectableParams[propertyKey.toString()] = injectableParams[propertyKey.toString()] || [];
    injectableParams[propertyKey.toString()].push(parameterIndex);
  } else {
    // Decorador de parámetro en un constructor
    const constructorName = target.name;
    injectableParams[constructorName] = injectableParams[constructorName] || [];
    injectableParams[constructorName].push(parameterIndex);
  }
}

class DatabaseService {
  getConnection() { return "Conexión a DB"; }
}

class AuthService {
  constructor(@Inject private dbService: DatabaseService) {}

  authenticate() {
    console.log(`Autenticando con: ${this.dbService.getConnection()}`);
  }
}

// En un sistema de inyección de dependencias real, se resolverían automáticamente
// Aquí, simulamos la resolución:
function resolveDependencies<T>(constructor: new (...args: any[]) => T): T {
  const paramIndexes = injectableParams[constructor.name] || [];
  const args: any[] = [];
  
  // Para este ejemplo simple, asumimos que solo DatabaseService es inyectable
  // En un DI real, se mapearían los tipos de los parámetros
  for(let i=0; i < paramIndexes.length; i++) {
    if (paramIndexes[i] === 0) { // Asumimos que el primer parámetro es DatabaseService
      args.push(new DatabaseService());
    }
  }
  return new constructor(...args);
}

const auth = resolveDependencies(AuthService);
auth.authenticate();
// Output: Autenticando con: Conexión a DB

Este decorador Inject registra los índices de los parámetros que deben ser inyectados. Un sistema de inyección de dependencias más sofisticado podría usar reflect-metadata para obtener el tipo de cada parámetro, permitiendo una inyección de dependencias robusta. La función resolveDependencies simula cómo un contenedor DI usaría estos metadatos.


💡 Creando tus Propios Decoradores: Guía Paso a Paso

La creación de decoradores personalizados es una habilidad clave. Aquí te presentamos una estructura general para cada tipo.

Decoradores sin Argumentos

Son funciones simples que toman los argumentos específicos del tipo de decorador y realizan su lógica.

// Decorador de clase
function SimpleClassDecorator(constructor: Function) {
  console.log(`Clase ${constructor.name} ha sido decorada.`);
}

@SimpleClassDecorator
class MySimpleClass {}

// Decorador de método
function SimpleMethodDecorator(target: any, propertyKey: string, descriptor: PropertyDescriptor) {
  console.log(`Método ${propertyKey} ha sido decorado.`);
}

class MyClassWithMethod {
  @SimpleMethodDecorator
  myMethod() {}
}

Decoradores con Argumentos (Decorator Factories)

La mayoría de las veces querrás pasar argumentos a tus decoradores. Para esto, necesitas crear un factory de decoradores, que es una función que recibe tus argumentos y retorna el decorador real.

// Factory de decoradores de clase
function CustomClassDecorator(message: string) {
  return function <T extends { new(...args: any[]): {} }>(constructor: T) {
    return class extends constructor {
      customMessage = message;
      constructor(...args: any[]) {
        super(...args);
        console.log(`Clase ${constructor.name} decorada con mensaje: ${this.customMessage}`);
      }
    };
  };
}

@CustomClassDecorator("¡Hola desde el decorador!")
class GreetableClass {
  greet() { return "Hola"; }
}

const gc = new GreetableClass();
console.log((gc as any).customMessage);

// Factory de decoradores de método
function LogInput(prefix: string) {
  return function (target: any, propertyKey: string, descriptor: PropertyDescriptor) {
    const originalMethod = descriptor.value;
    descriptor.value = function (...args: any[]) {
      console.log(`${prefix} - Llamando a ${propertyKey} con argumentos: ${JSON.stringify(args)}`);
      return originalMethod.apply(this, args);
    };
    return descriptor;
  };
}

class Calculator {
  @LogInput("DEBUG")
  add(a: number, b: number) {
    return a + b;
  }
}

const calc = new Calculator();
calc.add(5, 3);
// Output: DEBUG - Llamando a add con argumentos: [5,3]

🔗 Orden de Ejecución de los Decoradores

El orden en que se ejecutan los decoradores es crucial para entender su comportamiento. Se aplican de la siguiente manera:

  1. Parámetros decorados, luego Métodos/Accesores/Propiedades decorados.
  2. Decoradores de la misma categoría se ejecutan de abajo hacia arriba (o de derecha a izquierda si están en la misma línea).
  3. Finalmente, los Decoradores de Clase se ejecutan después de que todos los miembros de la clase han sido decorados, también de abajo hacia arriba (o derecha a izquierda).
📌 **Nota:** Si tienes múltiples decoradores en una misma línea, se aplican de derecha a izquierda, pero se 'evalúan' (es decir, el factory se ejecuta) de izquierda a derecha.
1. Decoradores de Parámetros Desde abajo hacia arriba 2. Propiedades, Métodos y Accesores Desde abajo hacia arriba 3. Decoradores de Clase Desde abajo hacia arriba ORDEN DE EJECUCIÓN

Ejemplo:

function Log(name: string) {
  console.log(`(1) Evaluando decorador ${name}`);
  return function (target: any, propertyKey: string, descriptor?: PropertyDescriptor) {
    console.log(`(2) Ejecutando decorador ${name} en ${propertyKey || target.name}`);
  };
}

function LogClass(name: string) {
  console.log(`(1) Evaluando decorador de clase ${name}`);
  return function (constructor: Function) {
    console.log(`(2) Ejecutando decorador de clase ${name} en ${constructor.name}`);
  };
}

@LogClass("C - Segundo")
@LogClass("C - Primero")
class ExampleClass {
  @Log("M - Segundo")
  @Log("M - Primero")
  method(@Log("P - Segundo") @Log("P - Primero") param: string) {
    console.log("Método ejecutado.");
  }
}

new ExampleClass();

/*
Output:
(1) Evaluando decorador de clase C - Segundo
(1) Evaluando decorador de clase C - Primero
(1) Evaluando decorador P - Segundo
(1) Evaluando decorador P - Primero
(1) Evaluando decorador M - Segundo
(1) Evaluando decorador M - Primero
(2) Ejecutando decorador P - Primero en method
(2) Ejecutando decorador P - Segundo en method
(2) Ejecutando decorador M - Primero en method
(2) Ejecutando decorador M - Segundo en method
(2) Ejecutando decorador de clase C - Primero en ExampleClass
(2) Ejecutando decorador de clase C - Segundo en ExampleClass
*/

Observa cómo las fases de evaluación (cuando se llaman los factories de decoradores) ocurren de arriba hacia abajo y de izquierda a derecha. Sin embargo, las fases de ejecución (cuando se llaman las funciones decoradoras reales) ocurren en el orden inverso para miembros y luego para la clase.


🎯 Casos de Uso Avanzados y Patrones Comunes

Los decoradores no son solo para añadir logs o validaciones simples. Sus capacidades se extienden a patrones de diseño más complejos y la integración con otras características del lenguaje.

Inyección de Dependencias (DI)

Como vimos en el ejemplo de los decoradores de parámetros, los decoradores son fundamentales para construir sistemas de Inyección de Dependencias. Frameworks como Angular y NestJS los usan intensamente para identificar servicios, componentes e inyectar dependencias automáticamente.

🔥 **Importante:** Para la inyección de dependencias, es común usar la librería `reflect-metadata`, que permite a los decoradores leer metadatos de tipo en tiempo de ejecución. Debes importar `import 'reflect-metadata';` una vez en tu punto de entrada.

Patrón de Inversión de Control (IoC)

Relacionado con la DI, los decoradores facilitan la implementación del patrón de Inversión de Control. En lugar de que una clase controle la creación de sus dependencias, un contenedor IoC (ayudado por los decoradores) se encarga de ello, invirtiendo el control.

ORMs y Modelos de Datos

Muchos Object-Relational Mappers (ORMs) en TypeScript, como TypeORM, utilizan decoradores para definir el esquema de la base de datos directamente en las clases de los modelos.

import { Entity, PrimaryGeneratedColumn, Column } from "typeorm";

@Entity()
export class User {
  @PrimaryGeneratedColumn()
  id: number;

  @Column({
    type: "varchar",
    length: 100,
    unique: true
  })
  firstName: string;

  @Column({
    nullable: true
  })
  lastName: string;

  @Column({
    default: true
  })
  isActive: boolean;
}

Aquí, @Entity, @PrimaryGeneratedColumn, y @Column son decoradores que añaden metadatos a la clase User y sus propiedades, indicando cómo deben ser mapeadas a una tabla de base de datos.

Aspect-Oriented Programming (AOP)

Los decoradores son una herramienta excelente para implementar conceptos de AOP, como el logging, la caché, la validación o la autenticación de forma transversal, sin modificar el código de negocio principal. El ejemplo de LogExecutionTime es un claro caso de AOP.


⚠️ Consideraciones y Mejores Prácticas

Aunque los decoradores son poderosos, su uso debe ser considerado.

Limitaciones de los Decoradores

  • Uso experimental: Aunque maduros, siguen siendo una característica experimental en TypeScript y una propuesta de Stage 3 para ECMAScript. Esto significa que la sintaxis o el comportamiento podrían cambiar ligeramente en el futuro.
  • Depuración: Pueden hacer que la depuración sea un poco más compleja, ya que la lógica se inyecta indirectamente. Usar source maps y un buen IDE es crucial.
  • Curva de aprendizaje: Para desarrolladores nuevos en el concepto, puede requerir un tiempo de adaptación.

Mejores Prácticas

  • Claridad y Consistencia: Usa decoradores cuando mejoren la claridad y reduzcan la verbosidad del código. Mantén un estilo consistente.
  • Fábricas de Decoradores: Casi siempre es mejor crear un factory de decoradores, incluso si no tomas argumentos, ya que esto te da la flexibilidad de añadir argumentos más tarde sin refactorizar el código donde se usan.
  • Composición: Puedes aplicar múltiples decoradores a un mismo elemento. Esto permite componer funcionalidades de manera modular.
  • Evita la sobreingeniería: No uses decoradores solo por usarlos. Evalúa si realmente aportan valor al diseño de tu aplicación.
  • Documentación: Documenta claramente lo que hace cada decorador, especialmente si son personalizados y complejos.
¿Qué es `reflect-metadata`?

reflect-metadata es una librería que añade un polyfill para la Reflect Metadata API de ECMAScript. Permite a los decoradores y otras partes del código leer y escribir metadatos en un objeto en tiempo de ejecución. Esto es especialmente útil para obtener el tipo de un parámetro en un constructor o método, lo cual es fundamental para sistemas de inyección de dependencias o serialización/deserialización. Para usarlo, simplemente debes importarlo una vez en tu aplicación: import 'reflect-metadata';.

Paso 1: Habilitar experimentalDecorators en tsconfig.json.
Paso 2: Entender los 5 tipos de decoradores (clase, método, accesor, propiedad, parámetro).
Paso 3: Aprender a crear fábricas de decoradores para usar argumentos.
Paso 4: Considerar el orden de ejecución para evitar sorpresas.
Paso 5: Aplicar decoradores en escenarios como DI, ORMs o AOP.
Paso 6: Seguir las mejores prácticas y documentar su uso.

📚 Conclusión

Los decoradores en TypeScript son una característica extremadamente potente que, cuando se usa correctamente, puede transformar la forma en que estructuras y organizas tu código. Permiten una programación más declarativa, la separación de preocupaciones y la creación de APIs elegantes y modulares. Desde la validación de propiedades hasta la inyección de dependencias y el mapeo de ORMs, las posibilidades son vastas.

Esperamos que esta guía te haya proporcionado una base sólida para empezar a dominar los decoradores y aplicarlos en tus propios proyectos de TypeScript. ¡Experimenta y descubre nuevas formas de utilizarlos para hacer tu código más limpio y potente!

Comentarios (0)

Aún no hay comentarios. ¡Sé el primero!