Italiano

Esplora i Decoratori TypeScript: programmazione con metadati, orientata agli aspetti e miglioramento del codice con pattern dichiarativi. Guida completa per sviluppatori.

Decoratori TypeScript: Padroneggiare i Pattern di Programmazione con Metadati per Applicazioni Robuste

Nel vasto panorama dello sviluppo software moderno, mantenere basi di codice pulite, scalabili e gestibili è di primaria importanza. TypeScript, con il suo potente sistema di tipi e funzionalità avanzate, fornisce agli sviluppatori gli strumenti per raggiungere questo obiettivo. Tra le sue funzionalità più intriganti e trasformative ci sono i Decoratori. Sebbene siano ancora una funzionalità sperimentale al momento della stesura (proposta Stage 3 per ECMAScript), i decoratori sono ampiamente utilizzati in framework come Angular e TypeORM, cambiando radicalmente il nostro approccio ai pattern di progettazione, alla programmazione con metadati e alla programmazione orientata agli aspetti (AOP).

Questa guida completa approfondirà i decoratori TypeScript, esplorando la loro meccanica, i vari tipi, le applicazioni pratiche e le migliori pratiche. Che tu stia costruendo applicazioni enterprise su larga scala, microservizi o interfacce web lato client, comprendere i decoratori ti consentirà di scrivere codice TypeScript più dichiarativo, mantenibile e potente.

Comprendere il Concetto Fondamentale: Cos'è un Decoratore?

In sintesi, un decoratore è un tipo speciale di dichiarazione che può essere allegato a una dichiarazione di classe, metodo, accessor, proprietà o parametro. I decoratori sono funzioni che restituiscono un nuovo valore (o ne modificano uno esistente) per il target che stanno decorando. Il loro scopo principale è aggiungere metadati o cambiare il comportamento della dichiarazione a cui sono allegati, senza modificare direttamente la struttura del codice sottostante. Questo modo esterno e dichiarativo di aumentare il codice è incredibilmente potente.

Pensa ai decoratori come annotazioni o etichette che applichi a parti del tuo codice. Queste etichette possono quindi essere lette o agite da altre parti della tua applicazione o da framework, spesso a runtime, per fornire funzionalità o configurazioni aggiuntive.

La Sintassi di un Decoratore

I decoratori sono preceduti dal simbolo @, seguito dal nome della funzione del decoratore. Sono posizionati immediatamente prima della dichiarazione che stanno decorando.

@MyDecorator\nclass MyClass {\n  @AnotherDecorator\n  myMethod() {\n    // ...\n  }\n}

Abilitare i Decoratori in TypeScript

Prima di poter utilizzare i decoratori, devi abilitare l'opzione del compilatore experimentalDecorators nel tuo file tsconfig.json. Inoltre, per funzionalità avanzate di riflessione dei metadati (spesso utilizzate dai framework), avrai bisogno anche di emitDecoratorMetadata e del polyfill reflect-metadata.

// tsconfig.json\n{\n  \"compilerOptions\": {\n    \"target\": \"ES2017\",\n    \"module\": \"commonjs\",\n    \"experimentalDecorators\": true,\n    \"emitDecoratorMetadata\": true,\n    \"outDir\": \"./dist\",\n    \"strict\": true,\n    \"esModuleInterop\": true,\n    \"skipLibCheck\": true,\n    \"forceConsistentCasingInFileNames\": true\n  }\n}

Devi anche installare reflect-metadata:

npm install reflect-metadata --save\n# or\nyarn add reflect-metadata

E importarlo all'inizio del punto di ingresso della tua applicazione (ad es. main.ts o app.ts):

import \"reflect-metadata\";\n// Il codice della tua applicazione segue

Factory di Decoratori: Personalizzazione a Portata di Mano

Mentre un decoratore di base è una funzione, spesso avrai bisogno di passare argomenti a un decoratore per configurarne il comportamento. Ciò si ottiene utilizzando una factory di decoratori. Una factory di decoratori è una funzione che restituisce la funzione decoratore vera e propria. Quando applichi una factory di decoratori, la chiami con i suoi argomenti, ed essa restituisce la funzione decoratore che TypeScript applica al tuo codice.

Creazione di un Esempio Semplice di Factory di Decoratori

Creiamo una factory per un decoratore Logger che possa registrare messaggi con prefissi diversi.

function Logger(prefix: string) {\n  return function (target: Function) {\n    console.log(`[${prefix}] Class ${target.name} has been defined.`);\n  };\n}\n\n@Logger(\"APP_INIT\")\nclass ApplicationBootstrap {\n  constructor() {\n    console.log(\"Application is starting...\");\n  }\n}\n\nconst app = new ApplicationBootstrap();\n// Output:\n// [APP_INIT] Class ApplicationBootstrap has been defined.\n// Application is starting...

In questo esempio, Logger("APP_INIT") è la chiamata alla factory del decoratore. Restituisce la funzione decoratore effettiva che prende target: Function (il costruttore della classe) come argomento. Ciò consente una configurazione dinamica del comportamento del decoratore.

Tipi di Decoratori in TypeScript

TypeScript supporta cinque tipi distinti di decoratori, ciascuno applicabile a un tipo specifico di dichiarazione. La firma della funzione del decoratore varia in base al contesto a cui è applicata.

1. Decoratori di Classe

I decoratori di classe sono applicati alle dichiarazioni di classe. La funzione del decoratore riceve il costruttore della classe come unico argomento. Un decoratore di classe può osservare, modificare o persino sostituire una definizione di classe.

Firma:

function ClassDecorator(target: Function) { ... }

Valore di Ritorno:

Se il decoratore di classe restituisce un valore, sostituirà la dichiarazione di classe con la funzione costruttore fornita. Questa è una funzionalità potente, spesso utilizzata per mixin o aumento di classe. Se nessun valore viene restituito, viene utilizzata la classe originale.

Casi d'Uso:

Esempio di Decoratore di Classe: Iniezione di un Servizio

Immagina uno scenario semplice di iniezione di dipendenza in cui desideri contrassegnare una classe come "iniettabile" e, facoltativamente, fornirle un nome in un container.

const InjectableServiceRegistry = new Map<string, Function>();\n\nfunction Injectable(name?: string) {\n  return function<T extends { new(...args: any[]): {} }>(constructor: T) {\n    const serviceName = name || constructor.name;\n    InjectableServiceRegistry.set(serviceName, constructor);\n    console.log(`Registered service: ${serviceName}`);\n\n    // Optionally, you could return a new class here to augment behavior\n    return class extends constructor {\n      createdAt = new Date();\n      // Additional properties or methods for all injected services\n    };\n  };\n}\n\n@Injectable(\"UserService\")\nclass UserDataService {\n  getUsers() {\n    return [{ id: 1, name: \"Alice\" }, { id: 2, name: \"Bob\" }];\n  }\n}\n\n@Injectable()\nclass ProductDataService {\n  getProducts() {\n    return [{ id: 101, name: \"Laptop\" }, { id: 102, name: \"Mouse\" }];\n  }\n}\n\nconsole.log(\"--- Services Registered ---\");\nconsole.log(Array.from(InjectableServiceRegistry.keys()));\n\nconst userServiceConstructor = InjectableServiceRegistry.get(\"UserService\");\nif (userServiceConstructor) {\n  const userServiceInstance = new userServiceConstructor();\n  console.log(\"Users:\", userServiceInstance.getUsers());\n  // console.log(\"User Service Created At:\", userServiceInstance.createdAt); // If the returned class is used\n}

Questo esempio dimostra come un decoratore di classe possa registrare una classe e persino modificare il suo costruttore. Il decoratore Injectable rende la classe scopribile da un sistema teorico di iniezione di dipendenza.

2. Decoratori di Metodo

I decoratori di metodo sono applicati alle dichiarazioni di metodo. Ricevono tre argomenti: l'oggetto target (per i membri statici, la funzione costruttore; per i membri di istanza, il prototipo della classe), il nome del metodo e il descrittore di proprietà del metodo.

Firma:

function MethodDecorator(target: Object, propertyKey: string | symbol, descriptor: PropertyDescriptor) { ... }

Valore di Ritorno:

Un decoratore di metodo può restituire un nuovo PropertyDescriptor. Se lo fa, questo descrittore verrà utilizzato per definire il metodo. Ciò consente di modificare o sostituire l'implementazione originale del metodo, rendendolo incredibilmente potente per l'AOP.

Casi d'Uso:

Esempio di Decoratore di Metodo: Monitoraggio delle Prestazioni

Creiamo un decoratore MeasurePerformance per registrare il tempo di esecuzione di un metodo.

function MeasurePerformance(target: Object, propertyKey: string, descriptor: PropertyDescriptor) {\n  const originalMethod = descriptor.value;\n\n  descriptor.value = function(...args: any[]) {\n    const start = process.hrtime.bigint();\n    const result = originalMethod.apply(this, args);\n    const end = process.hrtime.bigint();\n    const duration = Number(end - start) / 1_000_000;\n    console.log(`Method \"${propertyKey}\" executed in ${duration.toFixed(2)} ms`);\n    return result;\n  };\n\n  return descriptor;\n}\n\nclass DataProcessor {\n  @MeasurePerformance\n  processData(data: number[]): number[] {\n    // Simula un'operazione complessa e dispendiosa in termini di tempo\n    for (let i = 0; i < 1_000_000; i++) {\n      Math.sin(i);\n    }\n    return data.map(n => n * 2);\n  }\n\n  @MeasurePerformance\n  fetchRemoteData(id: string): Promise<string> {\n    return new Promise(resolve => {\n      setTimeout(() => {\n        resolve(`Data for ID: ${id}`);\n      }, 500);\n    });\n  }\n}\n\nconst processor = new DataProcessor();\nprocessor.processData([1, 2, 3]);\nprocessor.fetchRemoteData(\"abc\").then(result => console.log(result));

Il decoratore MeasurePerformance avvolge il metodo originale con la logica di temporizzazione, stampando la durata dell'esecuzione senza ingombrare la logica di business all'interno del metodo stesso. Questo è un classico esempio di Programmazione Orientata agli Aspetti (AOP).

3. Decoratori di Accessor

I decoratori di accessor sono applicati alle dichiarazioni di accessor (get e set). Similmente ai decoratori di metodo, ricevono l'oggetto target, il nome dell'accessor e il suo descrittore di proprietà.

Firma:

function AccessorDecorator(target: Object, propertyKey: string | symbol, descriptor: PropertyDescriptor) { ... }

Valore di Ritorno:

Un decoratore di accessor può restituire un nuovo PropertyDescriptor, che verrà utilizzato per definire l'accessor.

Casi d'Uso:

Esempio di Decoratore di Accessor: Getters con Caching

Creiamo un decoratore che memorizzi nella cache il risultato di un costoso calcolo di un getter.

function CachedGetter(target: any, propertyKey: string, descriptor: PropertyDescriptor) {\n  const originalGetter = descriptor.get;\n  const cacheKey = `_cached_${String(propertyKey)}`;\n\n  if (originalGetter) {\n    descriptor.get = function() {\n      if (this[cacheKey] === undefined) {\n        console.log(`[Cache Miss] Computing value for ${String(propertyKey)}`);\n        this[cacheKey] = originalGetter.apply(this);\n      } else {\n        console.log(`[Cache Hit] Using cached value for ${String(propertyKey)}`);\n      }\n      return this[cacheKey];\n    };\n  }\n  return descriptor;\n}\n\nclass ReportGenerator {\n  private data: number[];\n\n  constructor(data: number[]) {\n    this.data = data;\n  }\n\n  // Simulates an expensive computation\n  @CachedGetter\n  get expensiveSummary(): number {\n    console.log(\"Performing expensive summary calculation...\");\n    return this.data.reduce((sum, current) => sum + current, 0) / this.data.length;\n  }\n}\n\nconst generator = new ReportGenerator([10, 20, 30, 40, 50]);\n\nconsole.log(\"First access:\", generator.expensiveSummary);\nconsole.log(\"Second access:\", generator.expensiveSummary);\nconsole.log(\"Third access:\", generator.expensiveSummary);

Questo decoratore assicura che il calcolo del getter expensiveSummary venga eseguito una sola volta, le chiamate successive restituiscono il valore memorizzato nella cache. Questo pattern è molto utile per ottimizzare le prestazioni dove l'accesso alle proprietà implica calcoli pesanti o chiamate esterne.

4. Decoratori di Proprietà

I decoratori di proprietà sono applicati alle dichiarazioni di proprietà. Ricevono due argomenti: l'oggetto target (per i membri statici, la funzione costruttore; per i membri di istanza, il prototipo della classe) e il nome della proprietà.

Firma:

function PropertyDecorator(target: Object, propertyKey: string | symbol) { ... }

Valore di Ritorno:

I decoratori di proprietà non possono restituire alcun valore. Il loro uso principale è registrare metadati sulla proprietà. Non possono cambiare direttamente il valore della proprietà o il suo descrittore al momento della decorazione, poiché il descrittore per una proprietà non è ancora completamente definito quando i decoratori di proprietà vengono eseguiti.

Casi d'Uso:

Esempio di Decoratore di Proprietà: Validazione Campo Obbligatorio

Creiamo un decoratore per contrassegnare una proprietà come "obbligatoria" e poi convalidarla a runtime.

interface ValidationRule {\n  property: string | symbol;\n  validate: (value: any) => boolean;\n  message: string;\n}\n\nconst validationRules: Map<Function, ValidationRule[]> = new Map();\n\nfunction Required(target: Object, propertyKey: string | symbol) {\n  const rules = validationRules.get(target.constructor) || [];\n  rules.push({\n    property: propertyKey,\n    validate: (value: any) => value !== null && value !== undefined && value !== \"\",\n    message: `${String(propertyKey)} is required.`\n  });\n  validationRules.set(target.constructor, rules);\n}\n\nfunction validate(instance: any): string[] {\n  const classRules = validationRules.get(instance.constructor) || [];\n  const errors: string[] = [];\n\n  for (const rule of classRules) {\n    if (!rule.validate(instance[rule.property])) {\n      errors.push(rule.message);\n    }\n  }\n  return errors;\n}\n\nclass UserProfile {\n  @Required\n  firstName: string;\n\n  @Required\n  lastName: string;\n\n  age?: number;\n\n  constructor(firstName: string, lastName: string, age?: number) {\n    this.firstName = firstName;\n    this.lastName = lastName;\n    this.age = age;\n  }\n}\n\nconst user1 = new UserProfile(\"John\", \"Doe\", 30);\nconsole.log(\"User 1 validation errors:\", validate(user1)); // []\n\nconst user2 = new UserProfile(\"\", \"Smith\");\nconsole.log(\"User 2 validation errors:\", validate(user2)); // [\"firstName is required.\"]\n\nconst user3 = new UserProfile(\"Alice\", \"\");\nconsole.log(\"User 3 validation errors:\", validate(user3)); // [\"lastName is required.\"]

Il decoratore Required registra semplicemente la regola di validazione con una mappa centrale validationRules. Una funzione validate separata utilizza quindi questi metadati per controllare l'istanza a runtime. Questo pattern separa la logica di validazione dalla definizione dei dati, rendendola riutilizzabile e pulita.

5. Decoratori di Parametro

I decoratori di parametro sono applicati ai parametri all'interno di un costruttore di classe o di un metodo. Ricevono tre argomenti: l'oggetto target (per i membri statici, la funzione costruttore; per i membri di istanza, il prototipo della classe), il nome del metodo (o undefined per i parametri del costruttore) e l'indice ordinale del parametro nell'elenco dei parametri della funzione.

Firma:

function ParameterDecorator(target: Object, propertyKey: string | symbol | undefined, parameterIndex: number) { ... }

Valore di Ritorno:

I decoratori di parametro non possono restituire alcun valore. Come i decoratori di proprietà, il loro ruolo primario è aggiungere metadati sul parametro.

Casi d'Uso:

Esempio di Decoratore di Parametro: Iniezione di Dati di Richiesta

Simuliamo come un framework web potrebbe usare i decoratori di parametro per iniettare dati specifici in un parametro di metodo, come un ID utente da una richiesta.

interface ParameterMetadata {\n  index: number;\n  key: string | symbol;\n  resolver: (request: any) => any;\n}\n\nconst parameterResolvers: Map<Function, Map<string | symbol, ParameterMetadata[]>> = new Map();\n\nfunction RequestParam(paramName: string) {\n  return function (target: Object, propertyKey: string | symbol | undefined, parameterIndex: number) {\n    const targetKey = propertyKey || \"constructor\";\n    let methodResolvers = parameterResolvers.get(target.constructor);\n    if (!methodResolvers) {\n      methodResolvers = new Map();\n      parameterResolvers.set(target.constructor, methodResolvers);\n    }\n    const paramMetadata = methodResolvers.get(targetKey) || [];\n    paramMetadata.push({\n      index: parameterIndex,\n      key: targetKey,\n      resolver: (request: any) => request[paramName]\n    });\n    methodResolvers.set(targetKey, paramMetadata);\n  };\n}\n\n// Una funzione framework ipotetica per invocare un metodo con parametri risolti\nfunction executeWithParams(instance: any, methodName: string, request: any) {\n  const classResolvers = parameterResolvers.get(instance.constructor);\n  if (!classResolvers) {\n    return (instance[methodName] as Function).apply(instance, []);\n  }\n  const methodParamMetadata = classResolvers.get(methodName);\n  if (!methodParamMetadata) {\n    return (instance[methodName] as Function).apply(instance, []);\n  }\n\n  const args: any[] = Array(methodParamMetadata.length);\n  for (const meta of methodParamMetadata) {\n    args[meta.index] = meta.resolver(request);\n  }\n  return (instance[methodName] as Function).apply(instance, args);\n}\n\nclass UserController {\n  getUser(@RequestParam(\"id\") userId: string, @RequestParam(\"token\") authToken?: string) {\n    console.log(`Fetching user with ID: ${userId}, Token: ${authToken || \"N/A\"}`);\n    return { id: userId, name: \"Jane Doe\" };\n  }\n\n  deleteUser(@RequestParam(\"id\") userId: string) {\n    console.log(`Deleting user with ID: ${userId}`);\n    return { status: \"deleted\", id: userId };\n  }\n}\n\nconst userController = new UserController();\n\n// Simula una richiesta in arrivo\nconst mockRequest = {\n  id: \"user123\",\n  token: \"abc-123\",\n  someOtherProp: \"xyz\"\n};\n\nconsole.log("\n--- Executing getUser ---");\nexecuteWithParams(userController, \"getUser\", mockRequest);\n\nconsole.log("\n--- Executing deleteUser ---");\nexecuteWithParams(userController, \"deleteUser\", { id: \"user456\" });

Questo esempio mostra come i decoratori di parametro possano raccogliere informazioni sui parametri di metodo richiesti. Un framework può quindi utilizzare questi metadati raccolti per risolvere e iniettare automaticamente i valori appropriati quando il metodo viene chiamato, semplificando significativamente la logica del controller o del servizio.

Composizione e Ordine di Esecuzione dei Decoratori

I decoratori possono essere applicati in varie combinazioni, e comprendere il loro ordine di esecuzione è cruciale per prevedere il comportamento ed evitare problemi inattesi.

Decoratori Multipli su un Singolo Target

Quando più decoratori vengono applicati a una singola dichiarazione (ad es., una classe, un metodo o una proprietà), essi vengono eseguiti in un ordine specifico: dal basso verso l'alto, o da destra a sinistra, per la loro valutazione. Tuttavia, i loro risultati vengono applicati nell'ordine opposto.

@DecoratorA\n@DecoratorB\nclass MyClass {\n  // ...\n}\n

Qui, DecoratorB verrà valutato per primo, poi DecoratorA. Se modificano la classe (ad es., restituendo un nuovo costruttore), la modifica di DecoratorA avvolgerà o si applicherà sulla modifica di DecoratorB.

Esempio: Concatenazione di Decoratori di Metodo

Consideriamo due decoratori di metodo: LogCall e Authorization.

function LogCall(target: any, propertyKey: string, descriptor: PropertyDescriptor) {\n  const originalMethod = descriptor.value;\n  descriptor.value = function (...args: any[]) {\n    console.log(`[LOG] Calling ${String(propertyKey)} with args:`, args);\n    const result = originalMethod.apply(this, args);\n    console.log(`[LOG] Method ${String(propertyKey)} returned:`, result);\n    return result;\n  };\n  return descriptor;\n}\n\nfunction Authorization(roles: string[]) {\n  return function (target: any, propertyKey: string, descriptor: PropertyDescriptor) {\n    const originalMethod = descriptor.value;\n    descriptor.value = function (...args: any[]) {\n      const currentUserRoles = [\"admin\"]; // Simula il recupero dei ruoli utente correnti\n      const authorized = roles.some(role => currentUserRoles.includes(role));\n      if (!authorized) {\n        console.warn(`[AUTH] Access denied for ${String(propertyKey)}. Required roles: ${roles.join(\", \")}`);\n        throw new Error(\"Unauthorized access\");\n      }\n      console.log(`[AUTH] Access granted for ${String(propertyKey)}`);\n      return originalMethod.apply(this, args);\n    };\n    return descriptor;\n  };\n}\n\nclass SecureService {\n  @LogCall\n  @Authorization([\"admin\"])\n  deleteSensitiveData(id: string) {\n    console.log(`Deleting sensitive data for ID: ${id}`);\n    return `Data ID ${id} deleted.`;\n  }\n\n  @Authorization([\"user\"])\n  @LogCall // Ordine cambiato qui\n  fetchPublicData(query: string) {\n    console.log(`Fetching public data with query: ${query}`);\n    return `Public data for query: ${query}`; \n  }\n}\n\nconst service = new SecureService();\n\ntry {\n  console.log("\n--- Calling deleteSensitiveData (Admin User) ---");\n  service.deleteSensitiveData(\"record123\");\n} catch (error: any) {\n  console.error(error.message);\n}\n\ntry {\n  console.log("\n--- Calling fetchPublicData (Non-Admin User) ---");\n  // Simula un utente non-admin che cerca di accedere a fetchPublicData che richiede il ruolo 'user'\n  const mockUserRoles = [\"guest\"]; // Questo fallirà l'autenticazione\n  // Per rendere ciò dinamico, avresti bisogno di un sistema DI o di un contesto statico per i ruoli utente correnti.\n  // Per semplicità, assumiamo che il decoratore Authorization abbia accesso al contesto utente corrente.\n  // Regoliamo il decoratore Authorization per assumere sempre 'admin' ai fini della demo, \n  // in modo che la prima chiamata abbia successo e la seconda fallisca per mostrare percorsi diversi.\n  \n  // Riesegui con il ruolo utente per fetchPublicData per avere successo.\n  // Immagina che currentUserRoles in Authorization diventi: ['user']\n  // Per questo esempio, manteniamolo semplice e mostriamo l'effetto dell'ordine.\n  service.fetchPublicData(\"search term\"); // Questo eseguirà Auth -> Log\n} catch (error: any) {\n  console.error(error.message);\n}\n\n/* Output atteso per deleteSensitiveData:\n[AUTH] Access granted for deleteSensitiveData\n[LOG] Calling deleteSensitiveData with args: [ 'record123' ]\nDeleting sensitive data for ID: record123\n[LOG] Method deleteSensitiveData returned: Data ID record123 deleted.\n*/\n\n/* Output atteso per fetchPublicData (se l'utente ha il ruolo 'user'):\n[LOG] Calling fetchPublicData with args: [ 'search term' ]\n[AUTH] Access granted for fetchPublicData\nFetching public data with query: search term\n[LOG] Method fetchPublicData returned: Public data for query: search term\n*/

Nota l'ordine: per deleteSensitiveData, Authorization (in basso) viene eseguito per primo, poi LogCall (in alto) lo avvolge. La logica interna di Authorization viene eseguita per prima. Per fetchPublicData, LogCall (in basso) viene eseguito per primo, poi Authorization (in alto) lo avvolge. Ciò significa che l'aspetto LogCall sarà al di fuori dell'aspetto Authorization. Questa differenza è critica per le preoccupazioni trasversali come il logging o la gestione degli errori, dove l'ordine di esecuzione può influenzare significativamente il comportamento.

Ordine di Esecuzione per Target Diversi

Quando una classe, i suoi membri e i parametri hanno tutti dei decoratori, l'ordine di esecuzione è ben definito:

  1. I Decoratori di Parametro vengono applicati per primi, per ciascun parametro, partendo dall'ultimo parametro fino al primo.
  2. Poi, i Decoratori di Metodo, Accessor o Proprietà vengono applicati per ciascun membro.
  3. Infine, i Decoratori di Classe vengono applicati alla classe stessa.

All'interno di ciascuna categoria, più decoratori sullo stesso target vengono applicati dal basso verso l'alto (o da destra a sinistra).

Esempio: Ordine di Esecuzione Completo

function log(message: string) {\n  return function (target: any, propertyKey: string | symbol | undefined, descriptorOrIndex?: PropertyDescriptor | number) {\n    if (typeof descriptorOrIndex === 'number') {\n      console.log(`Param Decorator: ${message} on parameter #${descriptorOrIndex} of ${String(propertyKey || \"constructor\")}`);\n    } else if (typeof propertyKey === 'string' || typeof propertyKey === 'symbol') {\n      if (descriptorOrIndex && 'value' in descriptorOrIndex && typeof descriptorOrIndex.value === 'function') {\n        console.log(`Method/Accessor Decorator: ${message} on ${String(propertyKey)}`);\n      } else {\n        console.log(`Property Decorator: ${message} on ${String(propertyKey)}`);\n      }\n    } else {\n      console.log(`Class Decorator: ${message} on ${target.name}`);\n    }\n    return descriptorOrIndex; // Restituisce il descrittore per metodo/accessor, undefined per gli altri\n  };\n}\n\n@log(\"Class Level D\")\n@log(\"Class Level C\")\nclass MyDecoratedClass {\n  @log(\"Static Property A\")\n  static staticProp: string = \"\";\n\n  @log(\"Instance Property B\")\n  instanceProp: number = 0;\n\n  @log(\"Method D\")\n  @log(\"Method C\")\n  myMethod(\n    @log(\"Parameter Z\") paramZ: string,\n    @log(\"Parameter Y\") paramY: number\n  ) {\n    console.log(\"Method myMethod executed.\");\n  }\n\n  @log(\"Getter/Setter F\")\n  get myAccessor() {\n    return \"\";\n  }\n\n  set myAccessor(value: string) {\n    //...\n  }\n\n  constructor() {\n    console.log(\"Constructor executed.\");\n  }\n}\n\nnew MyDecoratedClass();\n// Chiama il metodo per attivare il decoratore di metodo\nnew MyDecoratedClass().myMethod(\"hello\", 123);\n\n/* Output Previsto (approssimativo, a seconda della versione specifica di TypeScript e della compilazione):\nParam Decorator: Parameter Y on parameter #1 of myMethod\nParam Decorator: Parameter Z on parameter #0 of myMethod\nProperty Decorator: Static Property A on staticProp\nProperty Decorator: Instance Property B on instanceProp\nMethod/Accessor Decorator: Getter/Setter F on myAccessor\nMethod/Accessor Decorator: Method C on myMethod\nMethod/Accessor Decorator: Method D on myMethod\nClass Decorator: Class Level C on MyDecoratedClass\nClass Decorator: Class Level D on MyDecoratedClass\nConstructor executed.\nMethod myMethod executed.\n*/

Il tempismo esatto del log della console potrebbe variare leggermente a seconda di quando un costruttore o un metodo viene invocato, ma l'ordine in cui le funzioni del decoratore stesse vengono eseguite (e quindi i loro effetti collaterali o valori restituiti applicati) segue le regole di cui sopra.

Applicazioni Pratiche e Pattern di Progettazione con i Decoratori

I decoratori, specialmente in combinazione con il polyfill reflect-metadata, aprono un nuovo regno di programmazione basata sui metadati. Ciò consente potenti pattern di progettazione che astraggono il boilerplate e le preoccupazioni trasversali.

1. Iniezione di Dipendenza (DI)

Uno degli usi più importanti dei decoratori è nei framework di Iniezione di Dipendenza (come @Injectable(), @Component() di Angular, ecc., o l'ampio uso della DI in NestJS). I decoratori consentono di dichiarare le dipendenze direttamente sui costruttori o sulle proprietà, consentendo al framework di istanziare e fornire automaticamente i servizi corretti.

Esempio: Iniezione Semplificata di Servizi

import \"reflect-metadata\"; // Essenziale per emitDecoratorMetadata\n\nconst INJECTABLE_METADATA_KEY = Symbol(\"injectable\");\nconst INJECT_METADATA_KEY = Symbol(\"inject\");\n\nfunction Injectable() {\n  return function (target: Function) {\n    Reflect.defineMetadata(INJECTABLE_METADATA_KEY, true, target);\n  };\n}\n\nfunction Inject(token: any) {\n  return function (target: Object, propertyKey: string | symbol, parameterIndex: number) {\n    const existingInjections: any[] = Reflect.getOwnMetadata(INJECT_METADATA_KEY, target, propertyKey) || [];\n    existingInjections[parameterIndex] = token;\n    Reflect.defineMetadata(INJECT_METADATA_KEY, existingInjections, target, propertyKey);\n  };\n}\n\nclass Container {\n  private static instances = new Map<any, any>();\n\n  static resolve<T>(target: { new (...args: any[]): T }): T {\n    if (Container.instances.has(target)) {\n      return Container.instances.get(target);\n    }\n\n    const isInjectable = Reflect.getMetadata(INJECTABLE_METADATA_KEY, target);\n    if (!isInjectable) {\n      throw new Error(`Class ${target.name} is not marked as @Injectable.`);\n    }\n\n    // Ottieni i tipi dei parametri del costruttore (richiede emitDecoratorMetadata)\n    const paramTypes: any[] = Reflect.getMetadata(\"design:paramtypes\", target) || [];\n    const explicitInjections: any[] = Reflect.getMetadata(INJECT_METADATA_KEY, target) || [];\n\n    const dependencies = paramTypes.map((paramType, index) => {\n      // Usa il token @Inject esplicito se fornito, altrimenti inferisci il tipo\n      const token = explicitInjections[index] || paramType;\n      if (token === undefined) {\n        throw new Error(`Cannot resolve parameter at index ${index} for ${target.name}. It might be a circular dependency or primitive type without explicit @Inject.`);\n      }\n      return Container.resolve(token);\n    });\n\n    const instance = new target(...dependencies);\n    Container.instances.set(target, instance);\n    return instance;\n  }\n}\n\n// Definisci i servizi\n@Injectable()\nclass DatabaseService {\n  connect() {\n    console.log(\"Connecting to database...\");\n    return \"DB Connection\";\n  }\n}\n\n@Injectable()\nclass AuthService {\n  private db: DatabaseService;\n\n  constructor(db: DatabaseService) {\n    this.db = db;\n  }\n\n  login() {\n    console.log(`AuthService: Authenticating using ${this.db.connect()}`);\n    return \"User logged in\";\n  }\n}\n\n@Injectable()\nclass UserService {\n  private authService: AuthService;\n  private dbService: DatabaseService; // Esempio di iniezione tramite proprietà usando un decoratore personalizzato o una funzionalità del framework\n\n  constructor(@Inject(AuthService) authService: AuthService,\n              @Inject(DatabaseService) dbService: DatabaseService) {\n    this.authService = authService;\n    this.dbService = dbService;\n  }\n\n  getUserProfile() {\n    this.authService.login();\n    this.dbService.connect();\n    console.log(\"UserService: Fetching user profile...\");\n    return { id: 1, name: \"Global User\" };\n  }\n}\n\n// Risolvi il servizio principale\nconsole.log("--- Resolving UserService ---");\nconst userService = Container.resolve(UserService);\nconsole.log(userService.getUserProfile());\n\nconsole.log("\n--- Resolving AuthService (should be cached) ---");\nconst authService = Container.resolve(AuthService);\nauthService.login();

Questo elaborato esempio dimostra come i decoratori @Injectable e @Inject, combinati con reflect-metadata, consentano a un Container personalizzato di risolvere e fornire automaticamente le dipendenze. I metadati design:paramtypes emessi automaticamente da TypeScript (quando emitDecoratorMetadata è true) sono cruciali qui.

2. Programmazione Orientata agli Aspetti (AOP)

L'AOP si concentra sulla modularizzazione delle preoccupazioni trasversali (ad es., logging, sicurezza, transazioni) che attraversano più classi e moduli. I decoratori si adattano perfettamente all'implementazione dei concetti AOP in TypeScript.

Esempio: Logging con Decoratore di Metodo

Rivedendo il decoratore LogCall, è un esempio perfetto di AOP. Aggiunge il comportamento di logging a qualsiasi metodo senza modificare il codice originale del metodo. Questo separa il "cosa fare" (logica di business) dal "come farlo" (logging, monitoraggio delle prestazioni, ecc.).

function LogMethod(target: any, propertyKey: string, descriptor: PropertyDescriptor) {\n  const originalMethod = descriptor.value;\n  descriptor.value = function (...args: any[]) {\n    console.log(`[LOG AOP] Entering method: ${String(propertyKey)} with args:`, args);\n    try {\n      const result = originalMethod.apply(this, args);\n      console.log(`[LOG AOP] Exiting method: ${String(propertyKey)} with result:`, result);\n      return result;\n    } catch (error: any) {\n      console.error(`[LOG AOP] Error in method ${String(propertyKey)}:`, error.message);\n      throw error;\n    }\n  };\n  return descriptor;\n}\n\nclass PaymentProcessor {\n  @LogMethod\n  processPayment(amount: number, currency: string) {\n    if (amount <= 0) {\n      throw new Error(\"Payment amount must be positive.\");\n    }\n    console.log(`Processing payment of ${amount} ${currency}...`);\n    return `Payment of ${amount} ${currency} processed successfully.`;\n  }\n\n  @LogMethod\n  refundPayment(transactionId: string) {\n    console.log(`Refunding payment for transaction ID: ${transactionId}...`);\n    return `Refund initiated for ${transactionId}.`;\n  }\n}\n\nconst processor = new PaymentProcessor();\nprocessor.processPayment(100, \"USD\");\ntry {\n  processor.processPayment(-50, \"EUR\");\n} catch (error: any) {\n  console.error(\"Caught error:\", error.message);\n}

Questo approccio mantiene la classe PaymentProcessor focalizzata puramente sulla logica di pagamento, mentre il decoratore LogMethod gestisce la preoccupazione trasversale del logging.

3. Validazione e Trasformazione

I decoratori sono incredibilmente utili per definire regole di validazione direttamente sulle proprietà o per trasformare i dati durante la serializzazione/deserializzazione.

Esempio: Validazione Dati con Decoratori di Proprietà

L'esempio @Required precedente lo ha già dimostrato. Ecco un altro esempio con una validazione di intervallo numerico.

interface FieldValidationRule {\n  property: string | symbol;\n  validator: (value: any) => boolean;\n  message: string;\n}\n\nconst fieldValidationRules = new Map<Function, FieldValidationRule[]>();\n\nfunction addValidationRule(target: Object, propertyKey: string | symbol, validator: (value: any) => boolean, message: string) {\n  const rules = fieldValidationRules.get(target.constructor) || [];\n  rules.push({ property: propertyKey, validator, message });\n  fieldValidationRules.set(target.constructor, rules);\n}\n\nfunction IsPositive(target: Object, propertyKey: string | symbol) {\n  addValidationRule(target, propertyKey, (value: number) => value > 0, `${String(propertyKey)} must be a positive number.`);\n}\n\nfunction MaxLength(maxLength: number) {\n  return function (target: Object, propertyKey: string | symbol) {\n    addValidationRule(target, propertyKey, (value: string) => value.length <= maxLength, `${String(propertyKey)} must be at most ${maxLength} characters long.`);\n  };\n}\n\nclass Product {\n  @MaxLength(50)\n  name: string;\n\n  @IsPositive\n  price: number;\n\n  constructor(name: string, price: number) {\n    this.name = name;\n    this.price = price;\n  }\n\n  static validate(instance: any): string[] {\n    const errors: string[] = [];\n    const rules = fieldValidationRules.get(instance.constructor) || [];\n    for (const rule of rules) {\n      if (!rule.validator(instance[rule.property])) {\n        errors.push(rule.message);\n      }\n    }\n    return errors;\n  }\n}\n\nconst product1 = new Product(\"Laptop\", 1200);\nconsole.log(\"Product 1 errors:\", Product.validate(product1)); // []\n\nconst product2 = new Product(\"Very long product name that exceeds fifty characters limit for testing purpose\", 50);\nconsole.log(\"Product 2 errors:\", Product.validate(product2)); // [\"name must be at most 50 characters long.\"]\n\nconst product3 = new Product(\"Book\", -10);\nconsole.log(\"Product 3 errors:\", Product.validate(product3)); // [\"price must be a positive number.\"]

Questa configurazione consente di definire in modo dichiarativo le regole di validazione sulle proprietà del modello, rendendo i modelli di dati auto-descrittivi in termini di vincoli.

Migliori Pratiche e Considerazioni

Sebbene i decoratori siano potenti, dovrebbero essere usati con giudizio. Usarli in modo improprio può portare a codice più difficile da debuggare o comprendere.

Quando Usare i Decoratori (e Quando No)

Implicazioni sulle Prestazioni

I decoratori vengono eseguiti al momento della compilazione (o della definizione in ambiente JavaScript runtime se trascompilati). La trasformazione o la raccolta dei metadati avviene quando la classe/metodo viene definita, non ad ogni chiamata. Pertanto, l'impatto sulle prestazioni a runtime dell'*applicazione* dei decoratori è minimo. Tuttavia, la *logica all'interno* dei tuoi decoratori può avere un impatto sulle prestazioni, specialmente se eseguono operazioni costose ad ogni chiamata di metodo (ad es., calcoli complessi all'interno di un decoratore di metodo).

Manutenibilità e Leggibilità

I decoratori, se usati correttamente, possono migliorare significativamente la leggibilità spostando il codice boilerplate fuori dalla logica principale. Tuttavia, se eseguono trasformazioni complesse e nascoste, il debug può diventare difficile. Assicurati che i tuoi decoratori siano ben documentati e che il loro comportamento sia prevedibile.

Stato Sperimentale e Futuro dei Decoratori

È importante ribadire che i decoratori TypeScript si basano su una proposta TC39 Stage 3. Ciò significa che la specifica è ampiamente stabile ma potrebbe ancora subire piccole modifiche prima di diventare parte dello standard ECMAScript ufficiale. Framework come Angular li hanno adottati, scommettendo sulla loro eventuale standardizzazione. Ciò implica un certo livello di rischio, sebbene, data la loro ampia adozione, modifiche significative che causano interruzioni siano improbabili.

La proposta TC39 si è evoluta. L'attuale implementazione di TypeScript si basa su una versione precedente della proposta. Esiste una distinzione tra "Decoratori Legacy" e "Decoratori Standard". Quando lo standard ufficiale verrà rilasciato, TypeScript probabilmente aggiornerà la sua implementazione. Per la maggior parte degli sviluppatori che utilizzano framework, questa transizione sarà gestita dal framework stesso. Per gli autori di librerie, comprendere le sottili differenze tra i decoratori legacy e quelli futuri standard potrebbe diventare necessario.

L'Opzione del Compilatore emitDecoratorMetadata

Questa opzione, quando impostata su true in tsconfig.json, istruisce il compilatore TypeScript a emettere determinati metadati di tipo in fase di progettazione nel JavaScript compilato. Questi metadati includono il tipo dei parametri del costruttore (design:paramtypes), il tipo di ritorno dei metodi (design:returntype) e il tipo delle proprietà (design:type).

Questi metadati emessi non fanno parte del runtime JavaScript standard. Sono tipicamente consumati dal polyfill reflect-metadata, che li rende poi accessibili tramite le funzioni Reflect.getMetadata(). Questo è assolutamente fondamentale per pattern avanzati come l'Iniezione di Dipendenza, dove un container ha bisogno di conoscere i tipi di dipendenze che una classe richiede senza una configurazione esplicita.

Pattern Avanzati con i Decoratori

I decoratori possono essere combinati ed estesi per costruire pattern ancora più sofisticati.

1. Decorare i Decoratori (Decoratori di Ordine Superiore)

È possibile creare decoratori che modificano o compongono altri decoratori. Questo è meno comune ma dimostra la natura funzionale dei decoratori.

// Un decoratore che assicura che un metodo sia loggato e richieda anche ruoli di amministratore\nfunction AdminAndLoggedMethod() {\n  return function (target: any, propertyKey: string, descriptor: PropertyDescriptor) {\n    // Applica prima l'autorizzazione (interna)\n    Authorization([\"admin\"])(target, propertyKey, descriptor);\n    // Poi applica LogCall (esterna)\n    LogCall(target, propertyKey, descriptor);\n\n    return descriptor; // Restituisce il descrittore modificato\n  };\n}\n\nclass AdminPanel {\n  @AdminAndLoggedMethod()\n  deleteUserAccount(userId: string) {\n    console.log(`Deleting user account: ${userId}`);\n    return `User ${userId} deleted.`;\n  }\n}\n\nconst adminPanel = new AdminPanel();\nadminPanel.deleteUserAccount(\"user007\");\n/* Output atteso (assumendo il ruolo di amministratore):\n[AUTH] Access granted for deleteUserAccount\n[LOG] Calling deleteUserAccount with args: [ 'user007' ]\nDeleting user account: user007\n[LOG] Method deleteUserAccount returned: User user007 deleted.\n*/

Qui, AdminAndLoggedMethod è una factory che restituisce un decoratore, e all'interno di quel decoratore, applica altri due decoratori. Questo pattern può incapsulare composizioni complesse di decoratori.

2. Utilizzo dei Decoratori per i Mixin

Sebbene TypeScript offra altri modi per implementare i mixin, i decoratori possono essere utilizzati per iniettare funzionalità nelle classi in modo dichiarativo.

function ApplyMixins(constructors: Function[]) {\n  return function (derivedConstructor: Function) {\n    constructors.forEach(baseConstructor => {\n      Object.getOwnPropertyNames(baseConstructor.prototype).forEach(name => {\n        Object.defineProperty(\n          derivedConstructor.prototype,\n          name,\n          Object.getOwnPropertyDescriptor(baseConstructor.prototype, name) || Object.create(null)\n        );\n      });\n    });\n  };\n}\n\nclass Disposable {\n  isDisposed: boolean = false;\n  dispose() {\n    this.isDisposed = true;\n    console.log(\"Object disposed.\");\n  }\n}\n\nclass Loggable {\n  log(message: string) {\n    console.log(`[Loggable] ${message}`);\n  }\n}\n\n@ApplyMixins([Disposable, Loggable])\nclass MyResource implements Disposable, Loggable {\n  // Queste proprietà/metodi sono iniettati dal decoratore\n  isDisposed!: boolean;\n  dispose!: () => void;\n  log!: (message: string) => void;\n\n  constructor(public name: string) {\n    this.log(`Resource ${this.name} created.`);\n  }\n\n  cleanUp() {\n    this.dispose();\n    this.log(`Resource ${this.name} cleaned up.`);\n  }\n}\n\nconst resource = new MyResource(\"NetworkConnection\");\nconsole.log(`Is disposed: ${resource.isDisposed}`);\nresource.cleanUp();\nconsole.log(`Is disposed: ${resource.isDisposed}`);

Questo decoratore @ApplyMixins copia dinamicamente metodi e proprietà dai costruttori base al prototipo della classe derivata, "miscelando" efficacemente le funzionalità.

Conclusione: Potenziamento dello Sviluppo TypeScript Moderno

I decoratori TypeScript sono una funzionalità potente ed espressiva che abilita un nuovo paradigma di programmazione basata sui metadati e orientata agli aspetti. Consentono agli sviluppatori di migliorare, modificare e aggiungere comportamenti dichiarativi a classi, metodi, proprietà, accessor e parametri senza alterare la loro logica centrale. Questa separazione delle preoccupazioni porta a codice più pulito, più manutenibile e altamente riutilizzabile.

Dalla semplificazione dell'iniezione di dipendenza e l'implementazione di robusti sistemi di validazione all'aggiunta di preoccupazioni trasversali come il logging e il monitoraggio delle prestazioni, i decoratori forniscono una soluzione elegante a molte comuni sfide di sviluppo. Sebbene il loro stato sperimentale giustifichi consapevolezza, la loro ampia adozione nei principali framework ne indica il valore pratico e la rilevanza futura.

Padroneggiando i decoratori TypeScript, si ottiene uno strumento significativo nel proprio arsenale, che consente di costruire applicazioni più robuste, scalabili e intelligenti. Abbracciali responsabilmente, comprendi la loro meccanica e sblocca un nuovo livello di potenza dichiarativa nei tuoi progetti TypeScript.