# 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...