Create a TypeScript custom decorator


What is a decorator?


Just a JavaScript function!


For more details, and according to the TypeScript documentation,


Decorators provide a way to add both annotations and a meta-programming syntax for class declarations and members. Decorators are a stage 2 proposal for JavaScript and are available as an experimental feature of TypeScript.


Decorators provide a way to extend methods, classes, properties and parameters with custom logic.


If you have already worked with Angular, you are probably familiar with those @Input() and @ViewChild() annotations, which are in fact TypeScript decorators!


To enable them (as they are experimental), you can set experimentalDecorators: true in your tsconfig.json file.


What does it look like?


To a simple JavaScript function 😄:


function logger(target) {
  // do something with 'target' ...
}


We can also write it as a "Factory".

A Decorator Factory is simply a function that returns the expression that will be called by the decorator at runtime, and allows it to pass arguments to the decorator.


function logger(type: string) {
  // this is the decorator factory, it sets up
  // the returned decorator functionreturn function (target) {
    // this is the decorator
    // do something with 'target' and 'value'...
  };
}


Example: Confirmation dialogs


In web applications, we often (always?) need to have confirmation dialogs to prevent users from performing important actions by mistake (delete, cancel...).


And we often end up with a lot of methods looking like this:


onDelete(ids: number[]): void {
    this.confirmationDialogService.open(
        {
            message: 'You are about to delete item(s). Continue?',
            accept: () => this.deleteItems(ids)
        }
    );
}


It would be nice to be able to only write something like:


@Confirmable // Custom decorator here
onDelete(ids: number[]): void {
    this.deleteItems(ids); // only declare the method to apply if user confirms
}


Well, that's what we are going to achieve here 🚀!


Implementation


First let's create a file confirmable.decorator.ts.


Inside, we need to export a function which we'll call Confirmable.


export function Confirmable (target: Object, propertyKey: string, descriptor: PropertyDescriptor) {
    // cache the original method for later useconst originalMethod = descriptor.value;
    // we write a new implementation for the method
    descriptor.value = async function (...args) {
        const message = 'Are you sure that you want to perform this action?';

        // ConfirmationDialogService must be injected to be used.// AppModule needs to be configured with injector, more on that later (*).
        const confirmationDialogService = AppModule.injector.get<ConfirmationDialogService>(
            ConfirmationDialogService
        );
        const res: boolean = await confirmationDialogService.open(message);
        if (res){ // if user clicked yes,// we run the original method with the original argumentsconst result = originalMethod.apply(this, args);
        // and return the result
        return result;
      }
    };
    return descriptor;
};


Now you can try to use it: it already works.


@Confirmable
onDelete(ids: number[]): void {
    this.deleteItems(ids);
}


(*) : in order to inject the confirmationDialogService into our new decorator, you will have to write this in your AppModule (or the module where the service is provided):

export class AppModule {
  static injector: Injector;
  
  constructor(injector: Injector) {
    AppModule.injector = injector;
  }
}



Custom confirmation message as parameter


But what if we want to define custom confirmation messages to display to the user?


We would need to twist a bit our new decorator transforming it to a factory!


const DEFAULT_MESSAGE = 'Are you sure that you want to perform this action?';

export function Confirmable(message: string = DEFAULT_MESSAGE) {
    return (target: Object, propertyKey: string, descriptor: PropertyDescriptor) => {
        const originalMethod = descriptor.value;
        descriptor.value = async function (...args) {
            const confirmationDialogService = AppModule.injector.get<ConfirmationDialogService>(
                ConfirmationDialogService
            );
            const res = await confirmationDialogService.open(message);
            if (res) {
                const result = originalMethod.apply(this, args);
                return result;
            }
        };
        return descriptor;
    };
}


Here we go! Now we can define a custom message while using the decorator:


@Confirmable('You are going to delete item(s). Continue?')
onDelete(ids: number[]): void {
    this.deleteItems(ids);
}


And that's it for today, hope you learned something useful 😊.