Decorators are an experimental feature of TypeScript that can be attached to a class declaration, method, accessor, property, or parameter in the form @expression
. Read more about decorators here.
Although decorators are an experimental feature, TypeScript based server-side frameworks such as the following heavily use this feature:
If you have used NestJS, you may have seen decorators in controllers:
import { Controller, Get } from '@nestjs/common';
@Controller('/cats')
export class CatsController {
@Get()
findAll(): string {
return 'This action returns all cats';
}
}
You can do a lot of things in decorators. One of the things that is very useful is to add metadata to the decorator target using Reflection API.
Frameworks like NestJs expose plenty of framework APIs as decorators which attaches metadata to decorator target and after that Reflection API is used to access the attached metadata.
For example, the [@Controller](https://medium.com/@AuthenticationG)()\
decorator in a class will register the class in the metadata as a controller for the specified HTTP route. The metadata will be read by the framework to know which controller is responsible for which route.
Most of the times, I find myself using the same set of decorators together like this:
import { Controller, Get } from '@nestjs/common';
import { AuthenticationGuard, AccessControlGuard } from 'app/decorators';
@Controller({ path: '/admin/dashboard' })
@AuthenticationGuard()
@AccessControlGuard({ role: 'admin' })
export class AdminDashboardController {
@Get()
index(): string {
return 'dashboard data for admin';
}
}
@Controller({ path: '/admin/posts' })
@AuthenticationGuard()
@AccessControlGuard({ role: 'admin' })
export class AdminPostsController {
@Get()
findPaginated(): string {
return 'all posts for the page';
}
}
It may be fine if you have just a few controllers, but it gets hard to maintain if you have lots of controllers.
A neat way to organize a group of decorators that come together in your application is to expose a new decorator that composes the group of decorators internally.
Imagine how neat it would be if we could so something like this for the above two controller classes:
import { Controller, Get } from '@nestjs/common';
import { AdminController, AccessControlGuard } from 'app/decorators';
@AdminController({ path: '/dashboard' })
export class AdminDashboardController {
@Get()
index(): string {
return 'dashboard data for admin';
}
}
@AdminController({ path: '/posts' })
export class AdminPostsController {
@Get()
findPaginated(): string {
return 'all posts for the page';
}
}
Let’s try to implement the new decorator AdminController
. In theory, pseudo-code for the decorator could be something like this:
@AdminController(options) {
@Controller({ path: '/admin' + options.path })
@AuthenticationGuard()
@AccessControlGuard({ role: 'admin' })
}
Like with any decorator, it has to implement a certain type. I am specifically creating a class decorator in this post, but the idea of decorator composition should apply to other type of decorators as well. Here’s the actual implementation of the fictional AdminController
decorator that we have used in the above gists.
import { Controller, ControllerOptions } from '@nestjs/common';
import { join } from 'path';
import { AuthenticationGuard, AccessControlGuard } from 'app/guards';
expose function AdminController(options: ControllerOptions) {
return function (target: Function) {
Controller(join('/admin', options.path))(target);
AuthenticationGuard()(target);
AccessControlGuard({ role: 'admin' })(target);
};
}
Final Thoughts
Decorators are currently in stage 2 proposal for JavaScript, but they are already used heavily in the TypeScript community.
Please feel free to play around with decorator patterns like this and share tips and tricks with the awesome JavaScript community.
If you like the decorator composition pattern, you may also like this library that I recently authored based on the pattern.
Top comments (0)