TypeScript

TypeScript

Tutorial TypeScript: Decoradores básicos

Aprende los fundamentos de los decoradores en TypeScript con ejemplos prácticos para modificar clases y métodos de forma declarativa.

Aprende TypeScript y certifícate

¿Qué son decoradores?

Los decoradores son un patrón de diseño que permite modificar o extender el comportamiento de clases, métodos, propiedades o parámetros sin alterar su código fuente original. En TypeScript, los decoradores proporcionan una forma elegante y declarativa de añadir funcionalidades adicionales a elementos existentes.

Un decorador es esencialmente una función especial que se aplica a una declaración mediante la sintaxis @nombreDecorador. Esta sintaxis declarativa hace que el código sea más legible y expresivo, facilitando la comprensión de las capacidades adicionales.

Estructura básica de un decorador

La estructura básica de un decorador en TypeScript es una función que recibe ciertos parámetros dependiendo del tipo de elemento que está decorando:

// Ejemplo básico de un decorador
function miDecorador(target: any) {
  console.log("El decorador se ha ejecutado");
}

// Aplicando el decorador a una clase
@miDecorador
class Ejemplo {
  // Contenido de la clase
}

Los decoradores se ejecutan durante la definición de la clase, no cuando se crea una instancia, lo que los hace ideales para añadir metadatos o realizar transformaciones en tiempo de compilación.

Tipos de decoradores

En TypeScript podemos crear diferentes tipos de decoradores según el elemento que queramos modificar:

  • Decoradores de clase: Se aplican a la definición de una clase.
  • Decoradores de método: Se aplican a un método dentro de una clase.
  • Decoradores de propiedad: Se aplican a una propiedad de una clase.
  • Decoradores de parámetro: Se aplican a los parámetros de un método o constructor.
  • Decoradores de accesores: Se aplican a los getters y setters.

Habilitando decoradores en TypeScript

Para utilizar decoradores en TypeScript, necesitamos habilitar la característica en nuestro archivo tsconfig.json:

{
  "compilerOptions": {
    "target": "ES5",
    "experimentalDecorators": true
  }
}

Creando un decorador simple

Veamos un ejemplo sencillo de un decorador de clase que añade una propiedad a la clase:

function AgregarVersion(version: string) {
  return function(constructor: Function) {
    constructor.prototype.version = version;
  };
}

@AgregarVersion("1.0.0")
class MiAplicacion {
  nombre: string;
  
  constructor(nombre: string) {
    this.nombre = nombre;
  }
}

// Ahora podemos acceder a la propiedad version
const app = new MiAplicacion("Mi App");
console.log((app as any).version); // Salida: 1.0.0

En este ejemplo, el decorador AgregarVersion es una función de fábrica que devuelve la función decoradora real. Esto nos permite pasar parámetros al decorador.

Decoradores de clase

Los decoradores de clase son uno de los tipos más comunes y útiles en TypeScript. Se aplican directamente a la declaración de una clase y permiten modificar o extender su comportamiento. Estos decoradores reciben como parámetro el constructor de la clase, lo que nos permite manipular tanto la clase en sí como su prototipo.

Estructura básica

Un decorador de clase tiene la siguiente estructura:

function EstamparFecha(constructor: Function) {
  // Añadimos una propiedad a la clase
  constructor.prototype.fechaCreacion = new Date().toISOString();
  
  // Añadimos un método a la clase
  constructor.prototype.mostrarFecha = function() {
    console.log(`Clase creada el: ${this.fechaCreacion}`);
  };
}

// Aplicamos el decorador
@EstamparFecha
class Proyecto {
  nombre: string;
  
  constructor(nombre: string) {
    this.nombre = nombre;
  }
}

// Uso de la clase decorada
const proyecto = new Proyecto("Mi Proyecto");

// Accedemos a la propiedad y método añadidos (con casting)
console.log((proyecto as any).fechaCreacion);
(proyecto as any).mostrarFecha();

Este ejemplo muestra cómo un decorador puede añadir propiedades y métodos al prototipo de una clase existente, extendiendo su funcionalidad sin modificar su definición original.

Modificando el prototipo de la clase

Una de las aplicaciones más comunes de los decoradores de clase es añadir propiedades o métodos al prototipo:

function AgregarMetodo(constructor: Function) {
  constructor.prototype.saludar = function() {
    return "¡Hola desde el decorador!";
  };
}

@AgregarMetodo
class Persona {
  nombre: string;
  
  constructor(nombre: string) {
    this.nombre = nombre;
  }
}

// Necesitamos usar casting porque TypeScript no conoce el método añadido dinámicamente
const persona = new Persona("Ana");
console.log((persona as any).saludar()); // ¡Hola desde el decorador!

Decoradores de clase con parámetros

Para crear decoradores más flexibles, podemos implementar decoradores de fábrica que acepten parámetros:

function Componente(opciones: { selector: string, template: string }) {
  return function(constructor: Function) {
    constructor.prototype.opciones = opciones;
    constructor.prototype.renderizar = function() {
      const elemento = document.querySelector(opciones.selector);
      if (elemento) {
        elemento.innerHTML = opciones.template;
      }
    };
  };
}

@Componente({
  selector: '#mi-componente',
  template: '<h1>Componente personalizado</h1>'
})
class MiComponente {
  constructor() {
    // Lógica del componente
  }
}

const componente = new MiComponente();
(componente as any).renderizar();

Este patrón es muy similar al utilizado en frameworks como Angular para definir componentes.

Reemplazando la clase original

Los decoradores de clase también pueden reemplazar completamente la clase original devolviendo una nueva clase que extiende o modifica la original:

function Sellado<T extends { new(...args: any[]): {} }>(constructor: T) {
  // Devolvemos una nueva clase que extiende la original
  return class extends constructor {
    sellado = true;
    
    constructor(...args: any[]) {
      super(...args);
      console.log("Se ha creado una instancia sellada");
    }
  };
}

@Sellado
class Documento {
  contenido: string;
  
  constructor(contenido: string) {
    this.contenido = contenido;
  }
}

const doc = new Documento("Contenido confidencial");
console.log(doc); // { contenido: "Contenido confidencial", sellado: true }

En este ejemplo, el decorador Sellado devuelve una nueva clase que extiende la original, añadiendo una propiedad sellado y modificando el constructor para mostrar un mensaje.

Consideraciones de rendimiento

Es importante tener en cuenta que los decoradores de clase se ejecutan durante la definición de la clase, no durante la creación de instancias. Esto significa que operaciones costosas en decoradores solo afectan al tiempo de carga del módulo, no al rendimiento en tiempo de ejecución:

function Analizar(constructor: Function) {
  console.time("Análisis de clase");
  
  // Análisis de la estructura de la clase
  const propiedades = Object.getOwnPropertyNames(constructor.prototype);
  console.log(`La clase ${constructor.name} tiene ${propiedades.length} métodos`);
  
  console.timeEnd("Análisis de clase");
}

@Analizar
class ComponenteComplejo {
  // Múltiples métodos y propiedades
  metodo1() {}
  metodo2() {}
  // ...
}

// El análisis se ejecuta al cargar el módulo, no al crear instancias
const c1 = new ComponenteComplejo(); // No ejecuta el análisis nuevamente
const c2 = new ComponenteComplejo(); // No ejecuta el análisis nuevamente

Los decoradores de clase son una herramienta potente para implementar patrones de diseño complejos y extender la funcionalidad de nuestras clases de manera declarativa, mejorando la legibilidad y mantenibilidad del código.

Decoradores de método

Los decoradores de método permiten modificar el comportamiento de un método específico. Reciben tres parámetros:

  1. target: El prototipo de la clase
  2. propertyKey: El nombre del método
  3. descriptor: El descriptor de propiedad del método

Estructura básica

function decoradorMetodo(
  target: any,                // La clase prototipo
  propertyKey: string,        // El nombre del método
  descriptor: PropertyDescriptor  // El descriptor de propiedad del método
) {
  // Modificaciones al método
}

El parámetro descriptor es especialmente importante, ya que contiene la definición del método y nos permite modificar su comportamiento:

{
  value: Function,        // La función original del método
  writable: boolean,      // Si se puede sobrescribir
  enumerable: boolean,    // Si aparece en enumeraciones (ej: for...in)
  configurable: boolean   // Si se puede modificar o eliminar
}

Ejemplo básico de un decorador de método

Veamos un ejemplo sencillo que registra cuándo se llama a un método:

function Registrar(target: any, propertyKey: string, descriptor: PropertyDescriptor) {
  // Guardamos referencia al método original
  const metodoOriginal = descriptor.value;
  
  // Reemplazamos el método con nuestra versión modificada
  descriptor.value = function(...args: any[]) {
    console.log(`Llamada a ${propertyKey} con argumentos: ${JSON.stringify(args)}`);
    
    // Llamamos al método original y capturamos su resultado
    const resultado = metodoOriginal.apply(this, args);
    
    console.log(`${propertyKey} retornó: ${JSON.stringify(resultado)}`);
    return resultado;
  };
  
  return descriptor;
}

class Calculadora {
  @Registrar
  sumar(a: number, b: number): number {
    return a + b;
  }
}

const calc = new Calculadora();
calc.sumar(5, 3);
// Salida:
// Llamada a sumar con argumentos: [5,3]
// sumar retornó: 8

Este decorador de método añade capacidades de registro (logging) al método sumar, mostrando información antes y después de su ejecución, sin modificar la lógica del propio método.

Decoradores de método con parámetros

Para crear decoradores más flexibles, podemos implementar decoradores de fábrica que acepten parámetros:

function Repetir(veces: number) {
  return function(
    target: any,
    propertyKey: string,
    descriptor: PropertyDescriptor
  ) {
    const metodoOriginal = descriptor.value;
    
    descriptor.value = function(...args: any[]) {
      let resultado: any;
      
      for (let i = 0; i < veces; i++) {
        resultado = metodoOriginal.apply(this, args);
        console.log(`Ejecución ${i + 1}: ${resultado}`);
      }
      
      return resultado;
    };
    
    return descriptor;
  };
}

class Procesador {
  @Repetir(3)
  procesar(dato: string): string {
    return dato.toUpperCase();
  }
}

const proc = new Procesador();
proc.procesar("hola");
// Salida:
// Ejecución 1: HOLA
// Ejecución 2: HOLA
// Ejecución 3: HOLA

Relación con clases y métodos estudiados

Los decoradores complementan las clases y métodos que hemos estudiado anteriormente en el curso:

  • Los decoradores operan sobre las clases que aprendimos a crear, permitiéndonos extender su funcionalidad
  • Pueden modificar métodos sin alterar su implementación original
  • Respetan los modificadores de acceso (public, private, protected)
  • Funcionan tanto con métodos de instancia como con métodos estáticos

Esta característica proporciona una capa adicional de abstracción y reutilización que se integra perfectamente con el paradigma orientado a objetos.

Parámetros de decoradores y aplicaciones prácticas

Decoradores con parámetros (decoradores de fábrica)

Para crear decoradores más flexibles que acepten configuraciones, usamos "decoradores de fábrica". Estos son funciones que retornan el decorador real:

// Decorador de fábrica que acepta parámetros
function RepetirVeces(veces: number) {
  // Esta función es el decorador real que se aplicará
  return function(target: any, propertyKey: string, descriptor: PropertyDescriptor) {
    const metodoOriginal = descriptor.value;
    
    descriptor.value = function(...args: any[]) {
      let resultado: any;
      
      for (let i = 0; i < veces; i++) {
        resultado = metodoOriginal.apply(this, args);
      }
      
      return resultado;
    };
    
    return descriptor;
  };
}

class Notificador {
  @RepetirVeces(3)
  enviarMensaje(mensaje: string): void {
    console.log(`Enviando: ${mensaje}`);
  }
}

const notif = new Notificador();
notif.enviarMensaje("¡Alerta importante!");
// Salida:
// Enviando: ¡Alerta importante!
// Enviando: ¡Alerta importante!
// Enviando: ¡Alerta importante!

Este patrón nos permite parametrizar nuestros decoradores, haciéndolos mucho más versátiles.

Ejemplo práctico: Validación de datos

Los decoradores son extremadamente útiles para implementar validaciones declarativas:

function ValidarNumeroPositivo(target: any, propertyKey: string, descriptor: PropertyDescriptor) {
  const metodoOriginal = descriptor.value;
  
  descriptor.value = function(...args: any[]) {
    // Validamos que todos los argumentos sean números positivos
    for (const arg of args) {
      if (typeof arg !== 'number' || arg <= 0) {
        throw new Error(`El método ${propertyKey} requiere números positivos`);
      }
    }
    
    return metodoOriginal.apply(this, args);
  };
  
  return descriptor;
}

class Rectangulo {
  @ValidarNumeroPositivo
  calcularArea(ancho: number, alto: number): number {
    return ancho * alto;
  }
}

const rect = new Rectangulo();
console.log(rect.calcularArea(5, 3)); // 15

try {
  rect.calcularArea(-2, 4); // Error
} catch (error) {
  console.error(error.message); // El método calcularArea requiere números positivos
}

Esta validación asegura que los argumentos cumplan ciertos criterios antes de ejecutar la lógica del método, mejorando la robustez del código.

Ejemplo práctico: Control de acceso

Los decoradores pueden implementar control de acceso o autenticación:

function SoloAdministrador(target: any, propertyKey: string, descriptor: PropertyDescriptor) {
  const metodoOriginal = descriptor.value;
  
  descriptor.value = function(...args: any[]) {
    // Verificamos si el usuario tiene el rol necesario
    if (!(this as any).usuario?.esAdmin) {
      throw new Error("Acceso denegado: Se requieren permisos de administrador");
    }
    
    return metodoOriginal.apply(this, args);
  };
  
  return descriptor;
}

class Panel {
  usuario: { nombre: string; esAdmin: boolean };
  
  constructor(usuario: { nombre: string; esAdmin: boolean }) {
    this.usuario = usuario;
  }
  
  @SoloAdministrador
  configurarSistema(): void {
    console.log("Sistema configurado correctamente");
  }
}

const admin = new Panel({ nombre: "Admin", esAdmin: true });
const usuario = new Panel({ nombre: "Usuario", esAdmin: false });

admin.configurarSistema(); // Sistema configurado correctamente

try {
  usuario.configurarSistema(); // Error
} catch (error) {
  console.error(error.message); // Acceso denegado: Se requieren permisos de administrador
}

Este ejemplo muestra cómo un decorador puede implementar lógica de control de acceso antes de permitir la ejecución de un método.

Combinando decoradores

Es posible aplicar múltiples decoradores a un mismo elemento. Se evalúan de arriba hacia abajo, pero se aplican de abajo hacia arriba:

function Decorador1(target: any, propertyKey: string, descriptor: PropertyDescriptor) {
  console.log("Aplicando Decorador1");
  const metodoOriginal = descriptor.value;
  
  descriptor.value = function(...args: any[]) {
    console.log("Inicio Decorador1");
    const resultado = metodoOriginal.apply(this, args);
    console.log("Fin Decorador1");
    return resultado;
  };
  
  return descriptor;
}

function Decorador2(target: any, propertyKey: string, descriptor: PropertyDescriptor) {
  console.log("Aplicando Decorador2");
  const metodoOriginal = descriptor.value;
  
  descriptor.value = function(...args: any[]) {
    console.log("Inicio Decorador2");
    const resultado = metodoOriginal.apply(this, args);
    console.log("Fin Decorador2");
    return resultado;
  };
  
  return descriptor;
}

class Ejemplo {
  @Decorador1
  @Decorador2
  metodo() {
    console.log("Ejecutando método");
    return "resultado";
  }
}

const ej = new Ejemplo();
ej.metodo();
// Salida:
// Aplicando Decorador2
// Aplicando Decorador1
// Inicio Decorador1
// Inicio Decorador2
// Ejecutando método
// Fin Decorador2
// Fin Decorador1

La combinación de decoradores permite crear capas de funcionalidad que se complementan entre sí.

Conclusión

Los decoradores proporcionan una forma elegante y declarativa de extender la funcionalidad de nuestras clases y métodos en TypeScript. Aunque son una característica experimental, su uso está muy extendido en frameworks modernos.

El poder de los decoradores radica en su capacidad para encapsular aspectos transversales (como logging, validación, control de acceso) fuera de la lógica principal del código, mejorando así la legibilidad, mantenibilidad y reutilización.

Los decoradores representan una forma más avanzada de aplicar los principios de la programación orientada a objetos que hemos estudiado anteriormente, permitiéndonos crear abstracciones más potentes y expresivas.

En futuras lecciones, exploraremos decoradores más avanzados y su aplicación en patrones de diseño complejos.

Aprende TypeScript online

Otros ejercicios de programación de TypeScript

Evalúa tus conocimientos de esta lección Decoradores básicos con nuestros retos de programación de tipo Test, Puzzle, Código y Proyecto con VSCode, guiados por IA.

Funciones

TypeScript
Test

Reto composición de funciones

TypeScript
Código

Reto tipos especiales

TypeScript
Código

Reto tipos genéricos

TypeScript
Código

Módulos

TypeScript
Test

Polimorfismo

TypeScript
Código

Funciones TypeScript

TypeScript
Código

Interfaces

TypeScript
Puzzle

Funciones puras

TypeScript
Puzzle

Reto namespaces

TypeScript
Código

Funciones flecha

TypeScript
Puzzle

Polimorfismo

TypeScript
Test

Operadores

TypeScript
Test

Conversor de unidades

TypeScript
Proyecto

Funciones flecha

TypeScript
Test

Control de flujo

TypeScript
Código

Herencia

TypeScript
Puzzle

Clases

TypeScript
Puzzle

Proyecto validación de tipado

TypeScript
Proyecto

Clases y objetos

TypeScript
Código

Encapsulación

TypeScript
Test

Herencia

TypeScript
Test

Proyecto sistema de votación

TypeScript
Proyecto

Reto genéricos con clases

TypeScript
Código

Inmutabilidad

TypeScript
Puzzle

Interfaces

TypeScript
Test

Funciones de alto orden

TypeScript
Test

Reto map y filter

TypeScript
Código

Control de flujo

TypeScript
Test

Interfaces

TypeScript
Código

Reto funciones orden superior

TypeScript
Código

Herencia y clases abstractas

TypeScript
Código

Reto tipos mapped

TypeScript
Código

Herencia de clases

TypeScript
Código

Reto funciones puras

TypeScript
Código

Variables y constantes

TypeScript
Puzzle

Introducción a TypeScript

TypeScript
Test

Reto testing unitario

TypeScript
Código

Funciones de primera clase

TypeScript
Puzzle

Clases

TypeScript
Test

OOP y CRUD en TypeScript

TypeScript
Proyecto

Interfaces y su implementación

TypeScript
Código

Tipos genéricos

TypeScript
Test

Namespaces

TypeScript
Test

Operadores y expresiones

TypeScript
Código

Proyecto generador de contraseñas

TypeScript
Proyecto

Reto unión e intersección

TypeScript
Código

Encapsulación

TypeScript
Puzzle

Tipos de unión e intersección

TypeScript
Test

Tipos de unión e intersección

TypeScript
Puzzle

Reto hola mundo en TS

TypeScript
Código

Variables y constantes

TypeScript
Código

Funciones puras

TypeScript
Test

Control de flujo

TypeScript
Código

Introducción a TypeScript

TypeScript
Código

Resolución de módulos

TypeScript
Test

Control de flujo

TypeScript
Puzzle

Reto tipos de utilidad

TypeScript
Código

Reto tipos literales y condicionales

TypeScript
Código

Reto exportar e importar

TypeScript
Código

Propiedades y métodos

TypeScript
Código

Tipos de utilidad

TypeScript
Test

Clases y objetos

TypeScript
Código

Tipos de datos, variables y constantes

TypeScript
Código

Proyecto Minigestor de tareas

TypeScript
Proyecto

Operadores

TypeScript
Puzzle

Funciones flecha y contexto

TypeScript
Código

Proyecto Inventario de productos

TypeScript
Proyecto

Funciones

TypeScript
Puzzle

Reto type aliases

TypeScript
Código

Funciones de alto orden

TypeScript
Puzzle

Funciones y parámetros tipados

TypeScript
Código

Tipos literales

TypeScript
Puzzle

Reto enums

TypeScript
Código

Tipos de utilidad

TypeScript
Puzzle

Modificadores de acceso y encapsulación

TypeScript
Código

Polimorfismo

TypeScript
Puzzle

Tipos genéricos

TypeScript
Puzzle

Reto módulos

TypeScript
Código

Tipos literales

TypeScript
Test

Inmutabilidad

TypeScript
Test

Proyecto Generator de datos

TypeScript
Proyecto

Variables y constantes

TypeScript
Test

Funciones de primera clase

TypeScript
Test

Todas las lecciones de TypeScript

Accede a todas las lecciones de TypeScript y aprende con ejemplos prácticos de código y ejercicios de programación con IDE web sin instalar nada.

Introducción A Typescript

TypeScript

Introducción Y Entorno

Instalación Y Configuración De Typescript

TypeScript

Introducción Y Entorno

Tipos De Datos, Variables Y Constantes

TypeScript

Sintaxis

Operadores Y Expresiones

TypeScript

Sintaxis

Control De Flujo

TypeScript

Sintaxis

Funciones Y Parámetros Tipados

TypeScript

Sintaxis

Funciones Flecha Y Contexto

TypeScript

Sintaxis

Enums

TypeScript

Sintaxis

Type Aliases Y Aserciones De Tipo

TypeScript

Sintaxis

Clases Y Objetos

TypeScript

Programación Orientada A Objetos

Interfaces Y Su Implementación

TypeScript

Programación Orientada A Objetos

Modificadores De Acceso Y Encapsulación

TypeScript

Programación Orientada A Objetos

Herencia Y Clases Abstractas

TypeScript

Programación Orientada A Objetos

Polimorfismo

TypeScript

Programación Orientada A Objetos

Decoradores Básicos

TypeScript

Programación Orientada A Objetos

Propiedades Y Métodos

TypeScript

Programación Orientada A Objetos

Inmutabilidad

TypeScript

Programación Funcional

Funciones Puras Y Efectos Secundarios

TypeScript

Programación Funcional

Funciones De Primera Clase

TypeScript

Programación Funcional

Funciones De Alto Orden

TypeScript

Programación Funcional

Conceptos Básicos E Inmutabilidad

TypeScript

Programación Funcional

Funciones De Primera Clase Y Orden Superior

TypeScript

Programación Funcional

Composición De Funciones

TypeScript

Programación Funcional

Métodos Funcionales De Arrays (Map, Filter, Reduce)

TypeScript

Programación Funcional

Tipos Literales Y Tipos Condicionales

TypeScript

Tipos Intermedios Y Avanzados

Tipos Genéricos Básicos

TypeScript

Tipos Intermedios Y Avanzados

Tipos De Unión E Intersección

TypeScript

Tipos Intermedios Y Avanzados

Tipos De Utilidad (Partial, Required, Pick, Etc)

TypeScript

Tipos Intermedios Y Avanzados

Unknown, Never Y Tipos Especiales

TypeScript

Tipos Intermedios Y Avanzados

Tipos Mapped

TypeScript

Tipos Intermedios Y Avanzados

Genéricos Con Clases E Interfaces

TypeScript

Tipos Intermedios Y Avanzados

Módulos

TypeScript

Namespaces Y Módulos

Namespaces

TypeScript

Namespaces Y Módulos

Resolución De Módulos

TypeScript

Namespaces Y Módulos

Exportación E Importación De Módulos

TypeScript

Namespaces Y Módulos

Introducción A Módulos

TypeScript

Namespaces Y Módulos

Testing Unitario En Typescript

TypeScript

Testing

Accede GRATIS a TypeScript y certifícate

En esta lección

Objetivos de aprendizaje de esta lección

  • Entender qué son los decoradores y cómo se habilitan en TypeScript.
  • Implementar decoradores de clase para extender funcionalidades existentes.
  • Crear decoradores de método que modifiquen el comportamiento de funciones.
  • Diseñar decoradores con parámetros para personalizar su funcionamiento.
  • Aplicar decoradores en casos prácticos como validación y control de acceso.