# Typescript Decorator Un decoratore è una funzione che prende la una classe e la arricchisce di specifiche funzionalità. Si tratta di uno standard introdotto dalle specifiche ECMAScript 2016 (ES7) e già recepite da TypeScript. ## Configurazione Per prima cosa bisogna impostare all'interno del file `tsconfig.json` questa configurazione ```json { "compilerOptions": { "target": "es2016", "experimentalDecorators": true } } ``` ## Tipologie Ci sono 4 principali tipi di decorator: - Class Decorator - Property Decorator - Method Decorator - Parameter Decorator ### Class Decorator È un decorator applicato ad una classe, riceve un parametro che rappresenta la classe da decorare. ```typescript declare type ClassDecorator = <TFunction extends Function> (target: TFunction) => TFunction | void; ``` Un esempio easy è il seguente: ```typescript function Greeter(target: Function): void { target.prototype.greet = function (): void { console.log("Ciao sono un utente!"); }; } @Greeter class User {} let user = new User(); (user as any).greet(); // -> "Ciao sono un utente!" ``` Il decorator `@Greeter` aggiunge alla classe sottostante il metodo `.greet()`. Se vogliamo customizzare l'output tramite il decorator dobbiamo utilizzare il **Decorator Factory**, es: ```typescript function Greeter(msg: string) { return (target: Function): void => { target.prototype.greet = function (): void { console.log(msg); }; } } @Greeter("Hello TypeScript!") class User {} let user = new User(); (user as any).greet(); // -> "Hello TypeScript!" ``` ### Property Decorator È un decorator applicato ad una proprietà di una classe, riceve due parametri che rappresentano la classe e la proprietà da decorare. ```typescript declare type PropertyDecorator = (target: Object, propertyKey: string | symbol) => void; ``` Un esempio è il seguente: ```typescript function logProperty(target: any, key: string) { let value = target[key] const getter = function () { console.log(`Getter for ${key} returned ${value}`) return value } const setter = function (newVal: any) { console.log(`Set ${key} to ${newVal}`) value = newVal } // Replace the property if (delete target[key]) { Object.defineProperty(target, key, { get: getter, set: setter, enumerable: true, configurable: true, }) } } class User { @logProperty public name: string constructor(name: string) { this.name = name } } const user = new User('Mario') user.name = 'Luca' console.log(user.name) ``` L'output in console sarebbe il seguente: ```bash Set name to Mario // -> from setter Set name to Luca // -> from setter Getter for name returned Luca // -> from getter Luca // -> from console.log and direct access ``` ### Method Decorator È un decorator applicato ad un meotodo di una classe, riceve tre parametri che rappresentano: la classe, il nome del metodo e un oggetto con le proprietà. ```typescript declare type MethodDecorator = <T>( target: Object, propertyKey: string | symbol, descriptor: TypedPropertyDescriptor<T> ) => TypedPropertyDescriptor<T> | void; interface TypedPropertyDescriptor<T> { enumerable?: boolean; configurable?: boolean; writable?: boolean; value?: T; get?: () => T; set?: (value: T) => void; } ``` Un esempio è il seguente: ```typescript function logMethod( target: Object, propertyKey: string, descriptor: PropertyDescriptor ) { let originalMethod = descriptor.value; descriptor.value = function (...args: any[]) { console.log(`Before invoking method: ${propertyKey}`); let result = originalMethod.apply(this, args); console.log(`After invoking method: ${propertyKey}`); return result; }; } class User { @logMethod public greet(msg: string) { console.log(`Hello ${msg}!`) } } let user = new User() user.greet('Mario') ``` L'output in console sarebbe il seguente: ```bash Before invoking method: greet Hello Mario! After invoking method: greet ``` ### Parameter Decorator È un decorator applicato ad un parametro di un metodo, riceve tre parametri che rappresentano: la classe, il metodo e l'indice del parametro da decorare. ```typescript declare type ParameterDecorator = ( target: Object, propertyKey: string | symbol, parameterIndex: number) => void; ``` Un esempio è il seguente: ```typescript function logParameter(target: Object, key: string, parameterIndex: number) { console.log(`The parameter in position ${parameterIndex} at ${key} has been decorated`); } class User { public greet(@logParameter msg: string): void { console.log(msg) } } let user = new User() user.greet('Mario') ``` L'output in console sarebbe il seguente: ```bash The parameter in position 0 at greet has been decorated Mario ``` ## Quindi? Tutto bello ma usiamo i decorator solo per fare i saluti? In questo [repository](https://github.com/NetanelBasal/helpful-decorators) si possono trovare molte implementazioni utili, es: ```typescript class Test { @delay(1000) method() { // Questo metodo verrà invocato un ritardo di un secondo } @once method() { // Questo metodo verrà invocato una sola volta } @measure doSomething() { // Call to doSomething took 0.35 milliseconds. } @SortBy('', { isDescending: false, type: 'number'}) numbers = [ 6, 3, 4, 1 ]; // in fase di get restituirà un array ordinato, es "[1, 3, 4, 6]" } ``` Sulla VMApp abbiamo implementato diversi decorator, tipo: ### CacheDecorator ```typescript import hash from 'object-hash' import CacheService from 'Services/CacheService' export default function CacheDecorator(seconds: number = 86400) { return function (target: any, propertyKey: string, descriptor: PropertyDescriptor) { const method = descriptor.value descriptor.value = CacheWrapper(method.bind(target), seconds) } } function CacheWrapper<T extends Array<any>, U extends any>( fn: (...args: T) => Promise<U>, seconds?: number ) { return async function (...args: T): Promise<U> { const key = hash({ args }) const cached = await CacheService.get(key) if (cached) { logger.info(`Returning cached data for key "${key}"`, { key }) return cached.data } const data = await fn(...args) if (data !== undefined) { logger.info(`Setting new cached data for key "${key}"`, { key, data, seconds }) CacheService.set(key, data, seconds) } return data } } ``` ```typescript class Test { @CacheDecorator() public getSomething() { // ... } } ``` ### TransactionDecorator ```typescript /** * Ensures that a transaction is passed to the method. Custom error can be passed * as optional argument. * * This decorator is supposed to be added only to instance methods where Transaction object * is the LAST ARGUMENT of the method signature. The decorator wraps the method inside * a function that check if a transaction is already started - i.e., is passed as the last argument * to the method call, or not. If no transaction is passed in, this function starts a new * transaction, calling the original method inside a try/catch group. * * Since this decorator wraps a single function call inside a transaction group, the decorator * is supposed to be called on a higher level than that of the single db queries. As an example * suppose we have three functions A, B, C, annotated with the TransactionDecorator, * all of them accepts a transaction as the last argument: * * @TransactionDecorator * function A(trx) { * B(trx) * C(trx) * } * * @TransactionDecorator * function B(trx) { * // some query * } * * @TransactionDecorator * function C(trx) { * // some query * } * * An A() call will init a new transaction and function B and C will receive it. * Yet in case B() or C() are called autonomously, the decorator will start a transaction * just for them singularly. * * @param {Error} customError */ export default function TransactionDecorator(customError?: Error) { return function (target: any, propertyKey: string, descriptor: PropertyDescriptor) { const method = descriptor.value descriptor.value = async function (...args: any[]) { if (transactionAlreadyStarted(args)) { // no need to start a new transaction, exit continuing with method call // @ts-ignore return method.call(this, ...args) } // start a new transaction and wrap the method call within it const transaction = await SequelizeConnection.transaction() try { // @ts-ignore const res = await method.call(this, ...args, transaction) await transaction.commit() return res } catch (e) { logger.error(e?.message, { error: e }) await transaction.rollback() throw customError ? customError : e } } } } /** * Returns true if and only if the last object of args array is of type transaction, * meaning that transaction is already started. * * @param {Array<any>} args */ function transactionAlreadyStarted(args: Array<any>): boolean { if (!args || !Array.isArray(args) || args.length <= 0) { return false } const lastArgument = args[args.length - 1] return lastArgument?.constructor?.name === 'Transaction' } ``` ```typescript class Test { @TransactionDecorator() public doSomething() { // ... } } ``` ### InterfaceValidatorDecorator ```typescript import { Schema } from 'joi' export default (schema: Schema) => { return function (target: Object, propertyKey: string, descriptor: PropertyDescriptor) { const targetMethod = descriptor.value descriptor.value = async function (...args: any[]) { const res = await targetMethod.apply(this, args) const validation = schema.validate(res, { allowUnknown: true, stripUnknown: true }) if (validation.error) { throw new Error(...) } return validation.value } return descriptor } } ``` ```typescript class Test { @InterfaceValidatorDecorator(DepartmentKpiBiSchema) public async getDepartmentsKpi(...): Promise<Array<DepartmentKpiBiInterface>> { // ... } } ``` ```typescript import Joi from 'joi' export interface DepartmentKpiBiInterface { department_code: string department_desc: string } const DepartmentKpiBiSchema = Joi.array().items( Joi.object({ department_code: Joi.string().trim(), department_desc: Joi.string(), }) ) export default DepartmentKpiBiSchema ``` ... Il limite è solo la vostra immaginazione...