Explore el poder de los decoradores de TypeScript para la programaci贸n con metadatos, orientada a aspectos y mejorar c贸digo con patrones declarativos. Una gu铆a completa para desarrolladores.
Decoradores de TypeScript: Dominando Patrones de Programaci贸n con Metadatos para Aplicaciones Robustas
En el vasto panorama del desarrollo de software moderno, mantener bases de c贸digo limpias, escalables y manejables es primordial. TypeScript, con su potente sistema de tipos y caracter铆sticas avanzadas, proporciona a los desarrolladores las herramientas para lograrlo. Entre sus caracter铆sticas m谩s intrigantes y transformadoras se encuentran los Decoradores. Aunque todav铆a es una caracter铆stica experimental en el momento de escribir este art铆culo (propuesta en Etapa 3 para ECMAScript), los decoradores son ampliamente utilizados en frameworks como Angular y TypeORM, cambiando fundamentalmente c贸mo abordamos los patrones de dise帽o, la programaci贸n con metadatos y la programaci贸n orientada a aspectos (AOP).
Esta gu铆a completa profundizar谩 en los decoradores de TypeScript, explorando su mec谩nica, diversos tipos, aplicaciones pr谩cticas y mejores pr谩cticas. Ya sea que est茅s construyendo aplicaciones empresariales a gran escala, microservicios o interfaces web del lado del cliente, comprender los decoradores te permitir谩 escribir c贸digo TypeScript m谩s declarativo, mantenible y potente.
Entendiendo el Concepto Central: 驴Qu茅 es un Decorador?
En esencia, un decorador es un tipo especial de declaraci贸n que se puede adjuntar a una declaraci贸n de clase, m茅todo, accesor, propiedad o par谩metro. Los decoradores son funciones que devuelven un nuevo valor (o modifican uno existente) para el objetivo que est谩n decorando. Su prop贸sito principal es agregar metadatos o cambiar el comportamiento de la declaraci贸n a la que se adjuntan, sin modificar directamente la estructura del c贸digo subyacente. Esta forma externa y declarativa de aumentar el c贸digo es incre铆blemente poderosa.
Piensa en los decoradores como anotaciones o etiquetas que aplicas a partes de tu c贸digo. Estas etiquetas pueden ser le铆das o utilizadas por otras partes de tu aplicaci贸n o por frameworks, a menudo en tiempo de ejecuci贸n, para proporcionar funcionalidad o configuraci贸n adicional.
La Sintaxis de un Decorador
Los decoradores llevan el prefijo del s铆mbolo @, seguido del nombre de la funci贸n del decorador. Se colocan inmediatamente antes de la declaraci贸n que est谩n decorando.
@MiDecorador
class MiClase {
@OtroDecorador
miMetodo() {
// ...
}
}
Habilitando Decoradores en TypeScript
Antes de poder usar decoradores, debes habilitar la opci贸n del compilador experimentalDecorators en tu archivo tsconfig.json. Adem谩s, para capacidades avanzadas de reflexi贸n de metadatos (a menudo utilizadas por frameworks), tambi茅n necesitar谩s emitDecoratorMetadata y el polyfill reflect-metadata.
// tsconfig.json
{
"compilerOptions": {
"target": "ES2017",
"module": "commonjs",
"experimentalDecorators": true,
"emitDecoratorMetadata": true,
"outDir": "./dist",
"strict": true,
"esModuleInterop": true,
"skipLibCheck": true,
"forceConsistentCasingInFileNames": true
}
}
Tambi茅n necesitas instalar reflect-metadata:
npm install reflect-metadata --save
# o
yarn add reflect-metadata
E importarlo en la parte superior del punto de entrada de tu aplicaci贸n (por ejemplo, main.ts o app.ts):
import "reflect-metadata";
// Tu c贸digo de aplicaci贸n va aqu铆
F谩bricas de Decoradores: Personalizaci贸n al Alcance de tu Mano
Aunque un decorador b谩sico es una funci贸n, a menudo necesitar谩s pasar argumentos a un decorador para configurar su comportamiento. Esto se logra usando una f谩brica de decoradores. Una f谩brica de decoradores es una funci贸n que devuelve la funci贸n del decorador real. Cuando aplicas una f谩brica de decoradores, la llamas con sus argumentos, y luego devuelve la funci贸n del decorador que TypeScript aplica a tu c贸digo.
Creando un Ejemplo Simple de F谩brica de Decoradores
Vamos a crear una f谩brica para un decorador Logger que pueda registrar mensajes con diferentes prefijos.
function Logger(prefix: string) {
return function (target: Function) {
console.log(`[${prefix}] La clase ${target.name} ha sido definida.`);
};
}
@Logger("APP_INIT")
class ApplicationBootstrap {
constructor() {
console.log("La aplicaci贸n se est谩 iniciando...");
}
}
const app = new ApplicationBootstrap();
// Salida:
// [APP_INIT] La clase ApplicationBootstrap ha sido definida.
// La aplicaci贸n se est谩 iniciando...
En este ejemplo, Logger("APP_INIT") es la llamada a la f谩brica de decoradores. Devuelve la funci贸n del decorador real que toma target: Function (el constructor de la clase) como su argumento. Esto permite la configuraci贸n din谩mica del comportamiento del decorador.
Tipos de Decoradores en TypeScript
TypeScript admite cinco tipos distintos de decoradores, cada uno aplicable a un tipo espec铆fico de declaraci贸n. La firma de la funci贸n del decorador var铆a seg煤n el contexto en el que se aplica.
1. Decoradores de Clase
Los decoradores de clase se aplican a las declaraciones de clase. La funci贸n del decorador recibe el constructor de la clase como su 煤nico argumento. Un decorador de clase puede observar, modificar o incluso reemplazar la definici贸n de una clase.
Firma:
function DecoradorDeClase(target: Function) { ... }
Valor de Retorno:
Si el decorador de clase devuelve un valor, reemplazar谩 la declaraci贸n de la clase con la funci贸n constructora proporcionada. Esta es una caracter铆stica poderosa, a menudo utilizada para mixins o para aumentar la clase. Si no se devuelve ning煤n valor, se utiliza la clase original.
Casos de Uso:
- Registrar clases en un contenedor de inyecci贸n de dependencias.
- Aplicar mixins o funcionalidades adicionales a una clase.
- Configuraciones espec铆ficas de un framework (por ejemplo, enrutamiento en un framework web).
- A帽adir hooks de ciclo de vida a las clases.
Ejemplo de Decorador de Clase: Inyectando un Servicio
Imagina un escenario simple de inyecci贸n de dependencias donde quieres marcar una clase como "inyectable" y opcionalmente proporcionar un nombre para ella en un contenedor.
const InjectableServiceRegistry = new Map<string, Function>();
function Injectable(name?: string) {
return function<T extends { new(...args: any[]): {} }>(constructor: T) {
const serviceName = name || constructor.name;
InjectableServiceRegistry.set(serviceName, constructor);
console.log(`Servicio registrado: ${serviceName}`);
// Opcionalmente, podr铆as devolver una nueva clase aqu铆 para aumentar el comportamiento
return class extends constructor {
createdAt = new Date();
// Propiedades o m茅todos adicionales para todos los servicios inyectados
};
};
}
@Injectable("UserService")
class UserDataService {
getUsers() {
return [{ id: 1, name: "Alice" }, { id: 2, name: "Bob" }];
}
}
@Injectable()
class ProductDataService {
getProducts() {
return [{ id: 101, name: "Laptop" }, { id: 102, name: "Mouse" }];
}
}
console.log("--- Servicios Registrados ---");
console.log(Array.from(InjectableServiceRegistry.keys()));
const userServiceConstructor = InjectableServiceRegistry.get("UserService");
if (userServiceConstructor) {
const userServiceInstance = new userServiceConstructor();
console.log("Usuarios:", userServiceInstance.getUsers());
// console.log("Servicio de Usuario Creado En:", userServiceInstance.createdAt); // Si se utiliza la clase devuelta
}
Este ejemplo demuestra c贸mo un decorador de clase puede registrar una clase e incluso modificar su constructor. El decorador Injectable hace que la clase sea descubrible por un sistema te贸rico de inyecci贸n de dependencias.
2. Decoradores de M茅todo
Los decoradores de m茅todo se aplican a las declaraciones de m茅todo. Reciben tres argumentos: el objeto de destino (para miembros est谩ticos, la funci贸n constructora; para miembros de instancia, el prototipo de la clase), el nombre del m茅todo y el descriptor de propiedad del m茅todo.
Firma:
function DecoradorDeMetodo(target: Object, propertyKey: string | symbol, descriptor: PropertyDescriptor) { ... }
Valor de Retorno:
Un decorador de m茅todo puede devolver un nuevo PropertyDescriptor. Si lo hace, este descriptor se usar谩 para definir el m茅todo. Esto te permite modificar o reemplazar la implementaci贸n del m茅todo original, lo que lo hace incre铆blemente potente para la AOP.
Casos de Uso:
- Registrar llamadas a m茅todos y sus argumentos/resultados.
- Almacenar en cach茅 los resultados de los m茅todos para mejorar el rendimiento.
- Aplicar comprobaciones de autorizaci贸n antes de la ejecuci贸n del m茅todo.
- Medir el tiempo de ejecuci贸n del m茅todo.
- Aplicar debouncing o throttling a las llamadas de m茅todo.
Ejemplo de Decorador de M茅todo: Monitoreo de Rendimiento
Vamos a crear un decorador MeasurePerformance para registrar el tiempo de ejecuci贸n de un m茅todo.
function MeasurePerformance(target: Object, propertyKey: string, descriptor: PropertyDescriptor) {
const originalMethod = descriptor.value;
descriptor.value = function(...args: any[]) {
const start = process.hrtime.bigint();
const result = originalMethod.apply(this, args);
const end = process.hrtime.bigint();
const duration = Number(end - start) / 1_000_000;
console.log(`El m茅todo "${propertyKey}" se ejecut贸 en ${duration.toFixed(2)} ms`);
return result;
};
return descriptor;
}
class DataProcessor {
@MeasurePerformance
processData(data: number[]): number[] {
// Simula una operaci贸n compleja y que consume tiempo
for (let i = 0; i < 1_000_000; i++) {
Math.sin(i);
}
return data.map(n => n * 2);
}
@MeasurePerformance
fetchRemoteData(id: string): Promise<string> {
return new Promise(resolve => {
setTimeout(() => {
resolve(`Datos para ID: ${id}`);
}, 500);
});
}
}
const processor = new DataProcessor();
processor.processData([1, 2, 3]);
processor.fetchRemoteData("abc").then(result => console.log(result));
El decorador MeasurePerformance envuelve el m茅todo original con l贸gica de temporizaci贸n, imprimiendo la duraci贸n de la ejecuci贸n sin saturar la l贸gica de negocio dentro del propio m茅todo. Este es un ejemplo cl谩sico de Programaci贸n Orientada a Aspectos (AOP).
3. Decoradores de Accesor
Los decoradores de accesor se aplican a las declaraciones de accesor (get y set). Al igual que los decoradores de m茅todo, reciben el objeto de destino, el nombre del accesor y su descriptor de propiedad.
Firma:
function DecoradorDeAccesor(target: Object, propertyKey: string | symbol, descriptor: PropertyDescriptor) { ... }
Valor de Retorno:
Un decorador de accesor puede devolver un nuevo PropertyDescriptor, que se utilizar谩 para definir el accesor.
Casos de Uso:
- Validaci贸n al establecer una propiedad.
- Transformar un valor antes de que se establezca o despu茅s de que se recupere.
- Controlar los permisos de acceso para las propiedades.
Ejemplo de Decorador de Accesor: Getters en Cach茅
Vamos a crear un decorador que almacene en cach茅 el resultado de un c谩lculo costoso de un getter.
function CachedGetter(target: any, propertyKey: string, descriptor: PropertyDescriptor) {
const originalGetter = descriptor.get;
const cacheKey = `_cached_${String(propertyKey)}`;
if (originalGetter) {
descriptor.get = function() {
if (this[cacheKey] === undefined) {
console.log(`[Cache Miss] Calculando valor para ${String(propertyKey)}`);
this[cacheKey] = originalGetter.apply(this);
} else {
console.log(`[Cache Hit] Usando valor en cach茅 para ${String(propertyKey)}`);
}
return this[cacheKey];
};
}
return descriptor;
}
class ReportGenerator {
private data: number[];
constructor(data: number[]) {
this.data = data;
}
// Simula un c谩lculo costoso
@CachedGetter
get expensiveSummary(): number {
console.log("Realizando c谩lculo de resumen costoso...");
return this.data.reduce((sum, current) => sum + current, 0) / this.data.length;
}
}
const generator = new ReportGenerator([10, 20, 30, 40, 50]);
console.log("Primer acceso:", generator.expensiveSummary);
console.log("Segundo acceso:", generator.expensiveSummary);
console.log("Tercer acceso:", generator.expensiveSummary);
Este decorador asegura que el c谩lculo del getter expensiveSummary solo se ejecute una vez; las llamadas posteriores devuelven el valor en cach茅. Este patr贸n es muy 煤til para optimizar el rendimiento donde el acceso a una propiedad implica un c谩lculo pesado o llamadas externas.
4. Decoradores de Propiedad
Los decoradores de propiedad se aplican a las declaraciones de propiedad. Reciben dos argumentos: el objeto de destino (para miembros est谩ticos, la funci贸n constructora; para miembros de instancia, el prototipo de la clase) y el nombre de la propiedad.
Firma:
function DecoradorDePropiedad(target: Object, propertyKey: string | symbol) { ... }
Valor de Retorno:
Los decoradores de propiedad no pueden devolver ning煤n valor. Su uso principal es registrar metadatos sobre la propiedad. No pueden cambiar directamente el valor de la propiedad o su descriptor en el momento de la decoraci贸n, ya que el descriptor de una propiedad a煤n no est谩 completamente definido cuando se ejecutan los decoradores de propiedad.
Casos de Uso:
- Registrar propiedades para serializaci贸n/deserializaci贸n.
- Aplicar reglas de validaci贸n a las propiedades.
- Establecer valores predeterminados o configuraciones para las propiedades.
- Mapeo de columnas en ORM (Object-Relational Mapping) (por ejemplo,
@Column()en TypeORM).
Ejemplo de Decorador de Propiedad: Validaci贸n de Campo Requerido
Vamos a crear un decorador para marcar una propiedad como "requerida" y luego validarla en tiempo de ejecuci贸n.
interface ValidationRule {
property: string | symbol;
validate: (value: any) => boolean;
message: string;
}
const validationRules: Map<Function, ValidationRule[]> = new Map();
function Required(target: Object, propertyKey: string | symbol) {
const rules = validationRules.get(target.constructor) || [];
rules.push({
property: propertyKey,
validate: (value: any) => value !== null && value !== undefined && value !== "",
message: `${String(propertyKey)} es requerido.`
});
validationRules.set(target.constructor, rules);
}
function validate(instance: any): string[] {
const classRules = validationRules.get(instance.constructor) || [];
const errors: string[] = [];
for (const rule of classRules) {
if (!rule.validate(instance[rule.property])) {
errors.push(rule.message);
}
}
return errors;
}
class UserProfile {
@Required
firstName: string;
@Required
lastName: string;
age?: number;
constructor(firstName: string, lastName: string, age?: number) {
this.firstName = firstName;
this.lastName = lastName;
this.age = age;
}
}
const user1 = new UserProfile("John", "Doe", 30);
console.log("Errores de validaci贸n del Usuario 1:", validate(user1)); // []
const user2 = new UserProfile("", "Smith");
console.log("Errores de validaci贸n del Usuario 2:", validate(user2)); // ["firstName es requerido."]
const user3 = new UserProfile("Alice", "");
console.log("Errores de validaci贸n del Usuario 3:", validate(user3)); // ["lastName es requerido."]
El decorador Required simplemente registra la regla de validaci贸n en un mapa central validationRules. Una funci贸n separada validate utiliza luego estos metadatos para verificar la instancia en tiempo de ejecuci贸n. Este patr贸n separa la l贸gica de validaci贸n de la definici贸n de datos, haci茅ndola reutilizable y limpia.
5. Decoradores de Par谩metro
Los decoradores de par谩metro se aplican a los par谩metros dentro de un constructor de clase o un m茅todo. Reciben tres argumentos: el objeto de destino (para miembros est谩ticos, la funci贸n constructora; para miembros de instancia, el prototipo de la clase), el nombre del m茅todo (o undefined para par谩metros de constructor) y el 铆ndice ordinal del par谩metro en la lista de par谩metros de la funci贸n.
Firma:
function DecoradorDeParametro(target: Object, propertyKey: string | symbol | undefined, parameterIndex: number) { ... }
Valor de Retorno:
Los decoradores de par谩metro no pueden devolver ning煤n valor. Al igual que los decoradores de propiedad, su funci贸n principal es agregar metadatos sobre el par谩metro.
Casos de Uso:
- Registrar tipos de par谩metros para inyecci贸n de dependencias (por ejemplo,
@Inject()en Angular). - Aplicar validaci贸n o transformaci贸n a par谩metros espec铆ficos.
- Extraer metadatos sobre los par谩metros de solicitud de API en frameworks web.
Ejemplo de Decorador de Par谩metro: Inyectando Datos de Solicitud
Simulemos c贸mo un framework web podr铆a usar decoradores de par谩metro para inyectar datos espec铆ficos en un par谩metro de m茅todo, como un ID de usuario de una solicitud.
interface ParameterMetadata {
index: number;
key: string | symbol;
resolver: (request: any) => any;
}
const parameterResolvers: Map<Function, Map<string | symbol, ParameterMetadata[]>> = new Map();
function RequestParam(paramName: string) {
return function (target: Object, propertyKey: string | symbol | undefined, parameterIndex: number) {
const targetKey = propertyKey || "constructor";
let methodResolvers = parameterResolvers.get(target.constructor);
if (!methodResolvers) {
methodResolvers = new Map();
parameterResolvers.set(target.constructor, methodResolvers);
}
const paramMetadata = methodResolvers.get(targetKey) || [];
paramMetadata.push({
index: parameterIndex,
key: targetKey,
resolver: (request: any) => request[paramName]
});
methodResolvers.set(targetKey, paramMetadata);
};
}
// Una funci贸n hipot茅tica de un framework para invocar un m茅todo con par谩metros resueltos
function executeWithParams(instance: any, methodName: string, request: any) {
const classResolvers = parameterResolvers.get(instance.constructor);
if (!classResolvers) {
return (instance[methodName] as Function).apply(instance, []);
}
const methodParamMetadata = classResolvers.get(methodName);
if (!methodParamMetadata) {
return (instance[methodName] as Function).apply(instance, []);
}
const args: any[] = Array(methodParamMetadata.length);
for (const meta of methodParamMetadata) {
args[meta.index] = meta.resolver(request);
}
return (instance[methodName] as Function).apply(instance, args);
}
class UserController {
getUser(@RequestParam("id") userId: string, @RequestParam("token") authToken?: string) {
console.log(`Obteniendo usuario con ID: ${userId}, Token: ${authToken || "N/A"}`);
return { id: userId, name: "Jane Doe" };
}
deleteUser(@RequestParam("id") userId: string) {
console.log(`Eliminando usuario con ID: ${userId}`);
return { status: "deleted", id: userId };
}
}
const userController = new UserController();
// Simula una solicitud entrante
const mockRequest = {
id: "user123",
token: "abc-123",
someOtherProp: "xyz"
};
console.log("\n--- Ejecutando getUser ---");
executeWithParams(userController, "getUser", mockRequest);
console.log("\n--- Ejecutando deleteUser ---");
executeWithParams(userController, "deleteUser", { id: "user456" });
Este ejemplo muestra c贸mo los decoradores de par谩metro pueden recopilar informaci贸n sobre los par谩metros de m茅todo requeridos. Un framework puede luego usar estos metadatos recopilados para resolver e inyectar autom谩ticamente los valores apropiados cuando se llama al m茅todo, simplificando significativamente la l贸gica del controlador o servicio.
Composici贸n de Decoradores y Orden de Ejecuci贸n
Los decoradores se pueden aplicar en varias combinaciones, y comprender su orden de ejecuci贸n es crucial para predecir el comportamiento y evitar problemas inesperados.
M煤ltiples Decoradores en un Solo Objetivo
Cuando se aplican m煤ltiples decoradores a una sola declaraci贸n (por ejemplo, una clase, m茅todo o propiedad), se ejecutan en un orden espec铆fico: de abajo hacia arriba, o de derecha a izquierda, para su evaluaci贸n. Sin embargo, sus resultados se aplican en el orden opuesto.
@DecoradorA
@DecoradorB
class MiClase {
// ...
}
Aqu铆, DecoradorB se evaluar谩 primero, luego DecoradorA. Si modifican la clase (por ejemplo, devolviendo un nuevo constructor), la modificaci贸n de DecoradorA envolver谩 o se aplicar谩 sobre la modificaci贸n de DecoradorB.
Ejemplo: Encadenamiento de Decoradores de M茅todo
Considera dos decoradores de m茅todo: LogCall y Authorization.
function LogCall(target: any, propertyKey: string, descriptor: PropertyDescriptor) {
const originalMethod = descriptor.value;
descriptor.value = function (...args: any[]) {
console.log(`[LOG] Llamando a ${String(propertyKey)} con args:`, args);
const result = originalMethod.apply(this, args);
console.log(`[LOG] M茅todo ${String(propertyKey)} devolvi贸:`, result);
return result;
};
return descriptor;
}
function Authorization(roles: string[]) {
return function (target: any, propertyKey: string, descriptor: PropertyDescriptor) {
const originalMethod = descriptor.value;
descriptor.value = function (...args: any[]) {
const currentUserRoles = ["admin"]; // Simula la obtenci贸n de roles del usuario actual
const authorized = roles.some(role => currentUserRoles.includes(role));
if (!authorized) {
console.warn(`[AUTH] Acceso denegado para ${String(propertyKey)}. Roles requeridos: ${roles.join(", ")}`);
throw new Error("Acceso no autorizado");
}
console.log(`[AUTH] Acceso concedido para ${String(propertyKey)}`);
return originalMethod.apply(this, args);
};
return descriptor;
};
}
class SecureService {
@LogCall
@Authorization(["admin"])
deleteSensitiveData(id: string) {
console.log(`Eliminando datos sensibles para ID: ${id}`);
return `Datos con ID ${id} eliminados.`;
}
@Authorization(["user"])
@LogCall // Orden cambiado aqu铆
fetchPublicData(query: string) {
console.log(`Obteniendo datos p煤blicos con consulta: ${query}`);
return `Datos p煤blicos para la consulta: ${query}`;
}
}
const service = new SecureService();
try {
console.log("\n--- Llamando a deleteSensitiveData (Usuario Admin) ---");
service.deleteSensitiveData("record123");
} catch (error: any) {
console.error(error.message);
}
try {
console.log("\n--- Llamando a fetchPublicData (Usuario no Admin) ---");
// Simula un usuario no administrador intentando acceder a fetchPublicData que requiere el rol 'user'
const mockUserRoles = ["guest"]; // Esto fallar谩 la autenticaci贸n
// Para que esto sea din谩mico, necesitar铆as un sistema de DI o un contexto est谩tico para los roles del usuario actual.
// Para simplificar, asumimos que el decorador Authorization tiene acceso al contexto del usuario actual.
// Ajustemos el decorador Authorization para que siempre asuma 'admin' para fines de demostraci贸n,
// para que la primera llamada tenga 茅xito y la segunda falle para mostrar diferentes caminos.
// Vuelve a ejecutar con el rol de usuario para que fetchPublicData tenga 茅xito.
// Imagina que currentUserRoles en Authorization se convierte en: ['user']
// Para este ejemplo, manteng谩moslo simple y mostremos el efecto del orden.
service.fetchPublicData("t茅rmino de b煤squeda"); // Esto ejecutar谩 Auth -> Log
} catch (error: any) {
console.error(error.message);
}
/* Salida esperada para deleteSensitiveData:
[AUTH] Acceso concedido para deleteSensitiveData
[LOG] Llamando a deleteSensitiveData con args: [ 'record123' ]
Eliminando datos sensibles para ID: record123
[LOG] M茅todo deleteSensitiveData devolvi贸: Datos con ID record123 eliminados.
*/
/* Salida esperada para fetchPublicData (si el usuario tiene el rol 'user'):
[LOG] Llamando a fetchPublicData con args: [ 't茅rmino de b煤squeda' ]
[AUTH] Acceso concedido para fetchPublicData
Obteniendo datos p煤blicos con consulta: t茅rmino de b煤squeda
[LOG] M茅todo fetchPublicData devolvi贸: Datos p煤blicos para la consulta: t茅rmino de b煤squeda
*/
Observa el orden: para deleteSensitiveData, Authorization (abajo) se ejecuta primero, luego LogCall (arriba) lo envuelve. La l贸gica interna de Authorization se ejecuta primero. Para fetchPublicData, LogCall (abajo) se ejecuta primero, luego Authorization (arriba) lo envuelve. Esto significa que el aspecto LogCall estar谩 fuera del aspecto Authorization. Esta diferencia es cr铆tica para los aspectos transversales como el registro de logs o el manejo de errores, donde el orden de ejecuci贸n puede impactar significativamente el comportamiento.
Orden de Ejecuci贸n para Diferentes Objetivos
Cuando una clase, sus miembros y par谩metros tienen todos decoradores, el orden de ejecuci贸n est谩 bien definido:
- Los Decoradores de Par谩metro se aplican primero, para cada par谩metro, comenzando desde el 煤ltimo par谩metro hasta el primero.
- Luego, los Decoradores de M茅todo, Accesor o Propiedad se aplican para cada miembro.
- Finalmente, los Decoradores de Clase se aplican a la clase misma.
Dentro de cada categor铆a, m煤ltiples decoradores en el mismo objetivo se aplican de abajo hacia arriba (o de derecha a izquierda).
Ejemplo: Orden de Ejecuci贸n Completo
function log(message: string) {
return function (target: any, propertyKey: string | symbol | undefined, descriptorOrIndex?: PropertyDescriptor | number) {
if (typeof descriptorOrIndex === 'number') {
console.log(`Decorador de Par谩metro: ${message} en el par谩metro #${descriptorOrIndex} de ${String(propertyKey || "constructor")}`);
} else if (typeof propertyKey === 'string' || typeof propertyKey === 'symbol') {
if (descriptorOrIndex && 'value' in descriptorOrIndex && typeof descriptorOrIndex.value === 'function') {
console.log(`Decorador de M茅todo/Accesor: ${message} en ${String(propertyKey)}`);
} else {
console.log(`Decorador de Propiedad: ${message} en ${String(propertyKey)}`);
}
} else {
console.log(`Decorador de Clase: ${message} en ${target.name}`);
}
return descriptorOrIndex; // Devuelve el descriptor para m茅todo/accesor, undefined para otros
};
}
@log("Nivel de Clase D")
@log("Nivel de Clase C")
class MyDecoratedClass {
@log("Propiedad Est谩tica A")
static staticProp: string = "";
@log("Propiedad de Instancia B")
instanceProp: number = 0;
@log("M茅todo D")
@log("M茅todo C")
myMethod(
@log("Par谩metro Z") paramZ: string,
@log("Par谩metro Y") paramY: number
) {
console.log("M茅todo myMethod ejecutado.");
}
@log("Getter/Setter F")
get myAccessor() {
return "";
}
set myAccessor(value: string) {
//...
}
constructor() {
console.log("Constructor ejecutado.");
}
}
new MyDecoratedClass();
// Llama al m茅todo para activar el decorador de m茅todo
new MyDecoratedClass().myMethod("hola", 123);
/* Orden de Salida Predicho (aproximado, dependiendo de la versi贸n espec铆fica de TypeScript y la compilaci贸n):
Decorador de Par谩metro: Par谩metro Y en el par谩metro #1 de myMethod
Decorador de Par谩metro: Par谩metro Z en el par谩metro #0 de myMethod
Decorador de Propiedad: Propiedad Est谩tica A en staticProp
Decorador de Propiedad: Propiedad de Instancia B en instanceProp
Decorador de M茅todo/Accesor: Getter/Setter F en myAccessor
Decorador de M茅todo/Accesor: M茅todo C en myMethod
Decorador de M茅todo/Accesor: M茅todo D en myMethod
Decorador de Clase: Nivel de Clase C en MyDecoratedClass
Decorador de Clase: Nivel de Clase D en MyDecoratedClass
Constructor ejecutado.
M茅todo myMethod ejecutado.
*/
El momento exacto del registro en la consola puede variar ligeramente seg煤n cu谩ndo se invoque un constructor o m茅todo, pero el orden en que las funciones del decorador se ejecutan (y, por lo tanto, se aplican sus efectos secundarios o valores devueltos) sigue las reglas anteriores.
Aplicaciones Pr谩cticas y Patrones de Dise帽o con Decoradores
Los decoradores, especialmente en conjunto con el polyfill reflect-metadata, abren un nuevo 谩mbito de programaci贸n impulsada por metadatos. Esto permite patrones de dise帽o potentes que abstraen el c贸digo repetitivo y los aspectos transversales.
1. Inyecci贸n de Dependencias (DI)
Uno de los usos m谩s prominentes de los decoradores es en los frameworks de Inyecci贸n de Dependencias (como @Injectable(), @Component(), etc. de Angular, o el uso extensivo de DI en NestJS). Los decoradores te permiten declarar dependencias directamente en constructores o propiedades, permitiendo que el framework instancie y proporcione autom谩ticamente los servicios correctos.
Ejemplo: Inyecci贸n de Servicio Simplificada
import "reflect-metadata"; // Esencial para emitDecoratorMetadata
const INJECTABLE_METADATA_KEY = Symbol("injectable");
const INJECT_METADATA_KEY = Symbol("inject");
function Injectable() {
return function (target: Function) {
Reflect.defineMetadata(INJECTABLE_METADATA_KEY, true, target);
};
}
function Inject(token: any) {
return function (target: Object, propertyKey: string | symbol, parameterIndex: number) {
const existingInjections: any[] = Reflect.getOwnMetadata(INJECT_METADATA_KEY, target, propertyKey) || [];
existingInjections[parameterIndex] = token;
Reflect.defineMetadata(INJECT_METADATA_KEY, existingInjections, target, propertyKey);
};
}
class Container {
private static instances = new Map<any, any>();
static resolve<T>(target: { new (...args: any[]): T }): T {
if (Container.instances.has(target)) {
return Container.instances.get(target);
}
const isInjectable = Reflect.getMetadata(INJECTABLE_METADATA_KEY, target);
if (!isInjectable) {
throw new Error(`La clase ${target.name} no est谩 marcada como @Injectable.`);
}
// Obtener los tipos de los par谩metros del constructor (requiere emitDecoratorMetadata)
const paramTypes: any[] = Reflect.getMetadata("design:paramtypes", target) || [];
const explicitInjections: any[] = Reflect.getMetadata(INJECT_METADATA_KEY, target) || [];
const dependencies = paramTypes.map((paramType, index) => {
// Usa el token expl铆cito de @Inject si se proporciona, de lo contrario, infiere el tipo
const token = explicitInjections[index] || paramType;
if (token === undefined) {
throw new Error(`No se puede resolver el par谩metro en el 铆ndice ${index} para ${target.name}. Podr铆a ser una dependencia circular o un tipo primitivo sin un @Inject expl铆cito.`);
}
return Container.resolve(token);
});
const instance = new target(...dependencies);
Container.instances.set(target, instance);
return instance;
}
}
// Definir servicios
@Injectable()
class DatabaseService {
connect() {
console.log("Conectando a la base de datos...");
return "Conexi贸n BD";
}
}
@Injectable()
class AuthService {
private db: DatabaseService;
constructor(db: DatabaseService) {
this.db = db;
}
login() {
console.log(`AuthService: Autenticando usando ${this.db.connect()}`);
return "Usuario autenticado";
}
}
@Injectable()
class UserService {
private authService: AuthService;
private dbService: DatabaseService; // Ejemplo de inyecci贸n a trav茅s de una propiedad usando un decorador personalizado o una caracter铆stica del framework
constructor(@Inject(AuthService) authService: AuthService,
@Inject(DatabaseService) dbService: DatabaseService) {
this.authService = authService;
this.dbService = dbService;
}
getUserProfile() {
this.authService.login();
this.dbService.connect();
console.log("UserService: Obteniendo perfil de usuario...");
return { id: 1, name: "Usuario Global" };
}
}
// Resolver el servicio principal
console.log("--- Resolviendo UserService ---");
const userService = Container.resolve(UserService);
console.log(userService.getUserProfile());
console.log("\n--- Resolviendo AuthService (deber铆a estar en cach茅) ---");
const authService = Container.resolve(AuthService);
authService.login();
Este ejemplo elaborado demuestra c贸mo los decoradores @Injectable y @Inject, combinados con reflect-metadata, permiten que un Container personalizado resuelva y proporcione dependencias autom谩ticamente. Los metadatos design:paramtypes emitidos autom谩ticamente por TypeScript (cuando emitDecoratorMetadata es true) son cruciales aqu铆.
2. Programaci贸n Orientada a Aspectos (AOP)
La AOP se enfoca en modularizar los aspectos transversales (por ejemplo, logging, seguridad, transacciones) que atraviesan m煤ltiples clases y m贸dulos. Los decoradores son una excelente opci贸n para implementar conceptos de AOP en TypeScript.
Ejemplo: Logging con Decorador de M茅todo
Volviendo al decorador LogCall, es un ejemplo perfecto de AOP. Agrega comportamiento de logging a cualquier m茅todo sin modificar el c贸digo original del m茅todo. Esto separa el "qu茅 hacer" (l贸gica de negocio) del "c贸mo hacerlo" (logging, monitoreo de rendimiento, etc.).
function LogMethod(target: any, propertyKey: string, descriptor: PropertyDescriptor) {
const originalMethod = descriptor.value;
descriptor.value = function (...args: any[]) {
console.log(`[LOG AOP] Entrando al m茅todo: ${String(propertyKey)} con args:`, args);
try {
const result = originalMethod.apply(this, args);
console.log(`[LOG AOP] Saliendo del m茅todo: ${String(propertyKey)} con resultado:`, result);
return result;
} catch (error: any) {
console.error(`[LOG AOP] Error en el m茅todo ${String(propertyKey)}:`, error.message);
throw error;
}
};
return descriptor;
}
class PaymentProcessor {
@LogMethod
processPayment(amount: number, currency: string) {
if (amount <= 0) {
throw new Error("El monto del pago debe ser positivo.");
}
console.log(`Procesando pago de ${amount} ${currency}...`);
return `Pago de ${amount} ${currency} procesado exitosamente.`;
}
@LogMethod
refundPayment(transactionId: string) {
console.log(`Reembolsando pago para el ID de transacci贸n: ${transactionId}...`);
return `Reembolso iniciado para ${transactionId}.`;
}
}
const processor = new PaymentProcessor();
processor.processPayment(100, "USD");
try {
processor.processPayment(-50, "EUR");
} catch (error: any) {
console.error("Error capturado:", error.message);
}
Este enfoque mantiene la clase PaymentProcessor enfocada puramente en la l贸gica de pago, mientras que el decorador LogMethod maneja el aspecto transversal del logging.
3. Validaci贸n y Transformaci贸n
Los decoradores son incre铆blemente 煤tiles para definir reglas de validaci贸n directamente en las propiedades o para transformar datos durante la serializaci贸n/deserializaci贸n.
Ejemplo: Validaci贸n de Datos con Decoradores de Propiedad
El ejemplo de @Required anterior ya demostr贸 esto. Aqu铆 hay otro ejemplo con una validaci贸n de rango num茅rico.
interface FieldValidationRule {
property: string | symbol;
validator: (value: any) => boolean;
message: string;
}
const fieldValidationRules = new Map<Function, FieldValidationRule[]>();
function addValidationRule(target: Object, propertyKey: string | symbol, validator: (value: any) => boolean, message: string) {
const rules = fieldValidationRules.get(target.constructor) || [];
rules.push({ property: propertyKey, validator, message });
fieldValidationRules.set(target.constructor, rules);
}
function IsPositive(target: Object, propertyKey: string | symbol) {
addValidationRule(target, propertyKey, (value: number) => value > 0, `${String(propertyKey)} debe ser un n煤mero positivo.`);
}
function MaxLength(maxLength: number) {
return function (target: Object, propertyKey: string | symbol) {
addValidationRule(target, propertyKey, (value: string) => value.length <= maxLength, `${String(propertyKey)} debe tener como m谩ximo ${maxLength} caracteres.`);
};
}
class Product {
@MaxLength(50)
name: string;
@IsPositive
price: number;
constructor(name: string, price: number) {
this.name = name;
this.price = price;
}
static validate(instance: any): string[] {
const errors: string[] = [];
const rules = fieldValidationRules.get(instance.constructor) || [];
for (const rule of rules) {
if (!rule.validator(instance[rule.property])) {
errors.push(rule.message);
}
}
return errors;
}
}
const product1 = new Product("Laptop", 1200);
console.log("Errores del producto 1:", Product.validate(product1)); // []
const product2 = new Product("Nombre de producto muy largo que excede el l铆mite de cincuenta caracteres para fines de prueba", 50);
console.log("Errores del producto 2:", Product.validate(product2)); // ["name debe tener como m谩ximo 50 caracteres."]
const product3 = new Product("Libro", -10);
console.log("Errores del producto 3:", Product.validate(product3)); // ["price debe ser un n煤mero positivo."]
Esta configuraci贸n te permite definir declarativamente reglas de validaci贸n en las propiedades de tu modelo, haciendo que tus modelos de datos se autodescriban en t茅rminos de sus restricciones.
Mejores Pr谩cticas y Consideraciones
Aunque los decoradores son potentes, deben usarse con prudencia. Un mal uso puede llevar a un c贸digo m谩s dif铆cil de depurar o entender.
Cu谩ndo Usar Decoradores (y Cu谩ndo No)
- 脷salos para:
- Aspectos transversales: Logging, cach茅, autorizaci贸n, gesti贸n de transacciones.
- Declaraci贸n de metadatos: Definir esquemas para ORMs, reglas de validaci贸n, configuraci贸n de DI.
- Integraci贸n con frameworks: Al construir o usar frameworks que aprovechan los metadatos.
- Reducir c贸digo repetitivo: Abstraer patrones de c贸digo repetitivos.
- Ev铆talos para:
- Llamadas a funciones simples: Si una simple llamada a una funci贸n puede lograr el mismo resultado de manera clara, prefi茅rela.
- L贸gica de negocio: Los decoradores deben aumentar, no definir, la l贸gica de negocio principal.
- Complicaci贸n excesiva: Si usar un decorador hace que el c贸digo sea menos legible o m谩s dif铆cil de probar, reconsid茅ralo.
Implicaciones de Rendimiento
Los decoradores se ejecutan en tiempo de compilaci贸n (o en tiempo de definici贸n en el entorno de ejecuci贸n de JavaScript si se transpila). La transformaci贸n o la recopilaci贸n de metadatos ocurre cuando se define la clase/m茅todo, no en cada llamada. Por lo tanto, el impacto en el rendimiento en tiempo de ejecuci贸n de *aplicar* decoradores es m铆nimo. Sin embargo, la *l贸gica dentro* de tus decoradores puede tener un impacto en el rendimiento, especialmente si realizan operaciones costosas en cada llamada a un m茅todo (por ejemplo, c谩lculos complejos dentro de un decorador de m茅todo).
Mantenibilidad y Legibilidad
Los decoradores, cuando se usan correctamente, pueden mejorar significativamente la legibilidad al mover el c贸digo repetitivo fuera de la l贸gica principal. Sin embargo, si realizan transformaciones complejas y ocultas, la depuraci贸n puede volverse un desaf铆o. Aseg煤rate de que tus decoradores est茅n bien documentados y que su comportamiento sea predecible.
Estado Experimental y Futuro de los Decoradores
Es importante reiterar que los decoradores de TypeScript se basan en una propuesta de Etapa 3 del TC39. Esto significa que la especificaci贸n es en gran medida estable, pero a煤n podr铆a sufrir cambios menores antes de convertirse en parte del est谩ndar oficial de ECMAScript. Frameworks como Angular los han adoptado, apostando por su eventual estandarizaci贸n. Esto implica un cierto nivel de riesgo, aunque dada su amplia adopci贸n, es poco probable que haya cambios importantes que rompan la compatibilidad.
La propuesta del TC39 ha evolucionado. La implementaci贸n actual de TypeScript se basa en una versi贸n anterior de la propuesta. Existe una distinci贸n entre "Decoradores Heredados" (Legacy Decorators) y "Decoradores Est谩ndar" (Standard Decorators). Cuando el est谩ndar oficial llegue, es probable que TypeScript actualice su implementaci贸n. Para la mayor铆a de los desarrolladores que usan frameworks, esta transici贸n ser谩 gestionada por el propio framework. Para los autores de bibliotecas, podr铆a ser necesario comprender las sutiles diferencias entre los decoradores heredados y los futuros decoradores est谩ndar.
La Opci贸n del Compilador emitDecoratorMetadata
Esta opci贸n, cuando se establece en true en tsconfig.json, instruye al compilador de TypeScript para que emita ciertos metadatos de tipo de tiempo de dise帽o en el JavaScript compilado. Estos metadatos incluyen el tipo de los par谩metros del constructor (design:paramtypes), el tipo de retorno de los m茅todos (design:returntype) y el tipo de las propiedades (design:type).
Estos metadatos emitidos no son parte del entorno de ejecuci贸n est谩ndar de JavaScript. T铆picamente son consumidos por el polyfill reflect-metadata, que luego los hace accesibles a trav茅s de las funciones Reflect.getMetadata(). Esto es absolutamente cr铆tico para patrones avanzados como la Inyecci贸n de Dependencias, donde un contenedor necesita saber los tipos de dependencias que una clase requiere sin una configuraci贸n expl铆cita.
Patrones Avanzados con Decoradores
Los decoradores se pueden combinar y extender para construir patrones a煤n m谩s sofisticados.
1. Decorando Decoradores (Decoradores de Orden Superior)
Puedes crear decoradores que modifiquen o compongan otros decoradores. Esto es menos com煤n pero demuestra la naturaleza funcional de los decoradores.
// Un decorador que asegura que un m茅todo se registre y tambi茅n requiera roles de administrador
function AdminAndLoggedMethod() {
return function (target: any, propertyKey: string, descriptor: PropertyDescriptor) {
// Aplicar Authorization primero (interno)
Authorization(["admin"])(target, propertyKey, descriptor);
// Luego aplicar LogCall (externo)
LogCall(target, propertyKey, descriptor);
return descriptor; // Devuelve el descriptor modificado
};
}
class AdminPanel {
@AdminAndLoggedMethod()
deleteUserAccount(userId: string) {
console.log(`Eliminando cuenta de usuario: ${userId}`);
return `Usuario ${userId} eliminado.`;
}
}
const adminPanel = new AdminPanel();
adminPanel.deleteUserAccount("user007");
/* Salida Esperada (asumiendo rol de admin):
[AUTH] Acceso concedido para deleteUserAccount
[LOG] Llamando a deleteUserAccount con args: [ 'user007' ]
Eliminando cuenta de usuario: user007
[LOG] M茅todo deleteUserAccount devolvi贸: Usuario user007 eliminado.
*/
Aqu铆, AdminAndLoggedMethod es una f谩brica que devuelve un decorador, y dentro de ese decorador, aplica otros dos decoradores. Este patr贸n puede encapsular composiciones complejas de decoradores.
2. Usando Decoradores para Mixins
Aunque TypeScript ofrece otras formas de implementar mixins, los decoradores se pueden usar para inyectar capacidades en las clases de forma declarativa.
function ApplyMixins(constructors: Function[]) {
return function (derivedConstructor: Function) {
constructors.forEach(baseConstructor => {
Object.getOwnPropertyNames(baseConstructor.prototype).forEach(name => {
Object.defineProperty(
derivedConstructor.prototype,
name,
Object.getOwnPropertyDescriptor(baseConstructor.prototype, name) || Object.create(null)
);
});
});
};
}
class Disposable {
isDisposed: boolean = false;
dispose() {
this.isDisposed = true;
console.log("Objeto desechado.");
}
}
class Loggable {
log(message: string) {
console.log(`[Loggable] ${message}`);
}
}
@ApplyMixins([Disposable, Loggable])
class MyResource implements Disposable, Loggable {
// Estas propiedades/m茅todos son inyectados por el decorador
isDisposed!: boolean;
dispose!: () => void;
log!: (message: string) => void;
constructor(public name: string) {
this.log(`Recurso ${this.name} creado.`);
}
cleanUp() {
this.dispose();
this.log(`Recurso ${this.name} limpiado.`);
}
}
const resource = new MyResource("NetworkConnection");
console.log(`Est谩 desechado: ${resource.isDisposed}`);
resource.cleanUp();
console.log(`Est谩 desechado: ${resource.isDisposed}`);
Este decorador @ApplyMixins copia din谩micamente m茅todos y propiedades de los constructores base al prototipo de la clase derivada, "mezclando" eficazmente funcionalidades.
Conclusi贸n: Potenciando el Desarrollo Moderno con TypeScript
Los decoradores de TypeScript son una caracter铆stica potente y expresiva que habilita un nuevo paradigma de programaci贸n orientada a aspectos e impulsada por metadatos. Permiten a los desarrolladores mejorar, modificar y agregar comportamientos declarativos a clases, m茅todos, propiedades, accesores y par谩metros sin alterar su l贸gica principal. Esta separaci贸n de intereses conduce a un c贸digo m谩s limpio, m谩s mantenible y altamente reutilizable.
Desde simplificar la inyecci贸n de dependencias e implementar sistemas de validaci贸n robustos hasta agregar aspectos transversales como el registro y el monitoreo del rendimiento, los decoradores proporcionan una soluci贸n elegante a muchos desaf铆os comunes del desarrollo. Si bien su estado experimental justifica estar al tanto, su amplia adopci贸n en los principales frameworks significa su valor pr谩ctico y su relevancia futura.
Al dominar los decoradores de TypeScript, obtienes una herramienta significativa en tu arsenal, que te permite construir aplicaciones m谩s robustas, escalables e inteligentes. Ad贸ptalos de manera responsable, comprende su mec谩nica y desbloquea un nuevo nivel de poder declarativo en tus proyectos de TypeScript.