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 {
&nbsp; static injector: Injector;
&nbsp; 
&nbsp; constructor(injector: Injector) {
&nbsp; &nbsp; AppModule.injector = injector;
&nbsp; }
}

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