typescript 装饰器 decorator
Decorators are one the most powerful features Typescript has to offer, allowing us to extend the functionality of classes and methods in a clean and declarative fashion. Decorators are currently a stage 2 proposal for JavaScript but have already gained popularity in the TypeScript eco-system, being used by leading open-source projects such as Angular and Inversify.
However awesome decorators may be, understanding the way they act might be mind-boggling. In this article, I aim to give you a solid understanding of decorators in less then 5 minutes of your time. So then, what are these decorators anyhow?
Decorators are just a clean syntax for wrapping a piece of code with a function
Decorators are an experimental feature, so implementation details might change in the future, but the underlying principles are eternal. To allow the use of decorators add "experimentalDecorators": true
to your compiler options in tsconfig.json
file, and make sure your transpilation target is ES5 or later.
Okay, the clock is ticking so let’s get coding!
The Journey Begins With Class Decorators
Say you have a business that rents old castles to powerful families, and you’re working on setting up an HTTP server. You decided to build each endpoint of your API as a class
, and the public methods of the class would correspond to the HTTP methods. This might look something like that:
class Families { private houses = ["Lannister", "Targaryen"]; get() { return this.houses; } post(request) { this.houses.push(request.body); } } class Castles { private castles = ["Winterfell", "Casterly Rock"]; get() { return this.castles; } post(request) { this.castles.push(request.body); } }
That’s a nice start, and now we need a simple way to “register” each of these classes as an endpoint in our HTTP server. Let’s create a simple function to take care of that. Our function will get a class
as an argument, and add an instance of that class as an endpoint to our server. Like so:
const httpEndpoints = {}; function registerEndpoint(constructor) { const className = constructor.name; const endpointPath = "/" + className.toLowerCase(); httpEndpoints[endpointPath] = new constructor(); } registerEndpoint(Families) registerEndpoint(Castles) console.log(httpEndpoints) // {"/families": Families, "/castles": Castles} httpEndpoints["/families"].get() // ["Lannister", "Targaryen"]
Without you noticing, we already wrote our first decorator! That’s right, it’s as simple as that. All a decorator is, is a function that takes a class as an argument, and here we have it. Now instead of calling registerEndpoint
in the “regular” way, we can just decorate our classes with @registerEndpoint
. don’t believe me? Have a look:
const httpEndpoints = {}; function registerEndpoint(constructor) { const className = constructor.name; const endpointPath = "/" + className.toLowerCase(); httpEndpoints[endpointPath] = new constructor(); } @registerEndpoint class Families { // implementation... } @registerEndpoint class Castles { // implementation... } console.log(httpEndpoints) // {"/families": Families, "/castles": Castles} httpEndpoints["/families"].get() // ["Lannister", "Targaryen"]
We got our first decorator up and running, and it only took us 2 minutes or so. We are now ready to dive deeper and unleash the power of the method decorator.
Unleash the Power of the Method Decorator
Let’s say that we want to protect some of our endpoints so that only authenticated users will be able to access them. To do that we can create a new decorator called protect
. For now, all our decorator will do is to add the protected method to an array called protectedMethods
.
const protectedMethods = []; function protect(target, propertyKey, descriptor) { const className = target.constructor.name; protectedMethods.push(className + "." + propertyKey); } @registerEndpoint class Families { private houses = ["Lannister", "Targaryen"]; @protect get() { return this.houses; } @protect post(request) { this.houses.push(request.body); } } console.log(protectedMethods) // ["Families.get", "Families.post"]
As you can see, the method decorator takes 3 arguments:
- target — The prototype of our class (or the constructor of the class if the decorated method is static).
- propertyKey — The name of the decorated method.
- descriptor — An object that holds the decorated function and some meta-data regarding it.
So far we only read information regarding the classes and methods we decorated, but the real fun begins when we start changing their behavior. We can do that by simply returning a value from the decorator. When a method decorator returns a value, this value will be used instead of the original descriptor (which holds the original method).
Let’s try it out by creating a decorator called nope
, that replaces our original method with a method that prints “nope” whenever it is called. To do that I’ll override descriptor.value
, which is where the original function is stored, with my function:
function nope(target, propertyKey, descriptor) { descriptor.value = function() { console.log("nope"); }; return descriptor; } @registerEndpoint class Families { private houses = ["Lannister", "Targaryen"]; @nope get() { return this.houses; } } httpEndpoints["/families"].get() // nope
By this point, we played around with the basics of method decorators, but haven’t created anything useful yet. What I plan to do next is rewrite the protect
decorator, but this time instead of only logging the names of the decorated methods, it will actually block unauthorised requests.
This next step will be a bit more complicated from the last ones so bear with me. Here are the steps we should take:
- Extract the original function from the descriptor, and store it somewhere else for later use.
- Override
descriptor.value
with a new function that takes the same arguments as the original. - Within our new function, check if the request is authenticated, and if not throw an error.
- Finally, also within our new function, we can now call the original function with the arguments it needs, capture its return value, and return it.
function protect(target, propertyKey, descriptor) { const originalFunction = descriptor.value; descriptor.value = function(request) { if (request.token !== "123") { throw new Error("forbiden!"); } const bindedOriginalFunction = originalFunction.bind(this) const result = bindedOriginalFunction(request) return result }; return descriptor; } @registerEndpoint class Families { private houses = ["Lannister", "Targaryen"]; @protect get() { return this.houses; } } httpEndpoints["/families"].get({token: "123"}) // ["Lannister", "Targaryen"] httpEndpoints["/families"].get({}) // Uncaught Error: forbiden!
Amazing! We have a fully operational protect
decorator. Now adding or removing protection from our methods is a piece of cake.
Note how in line 8 we bind the original function to this
, so it will have access to the instance of its class. For example, without this line, our get()
method wouldn’t be able to read this.houses
.
We’ve gone quite a way but there’s more ahead of us. At this point, I encourage you to take a moment and make sure you understand every bit of what we’ve done so far. To make it easier for you I put all of the code above in the playground here so you can run it, change it and break it until you feel you fully get it.
Gain More Power With Decorator Factories
Let’s say we now wish to add a new endpoint to return the Stark family members, at the path /families/stark/members
. Well, obviously we can’t create a class with that name, so what are we going to do?
What we need here is a way to pass parameters that will dictate the behavior of our decorator function. Let’s take another look at our good old registerEndpoint
decorator:
const httpEndpoints = {}; function registerEndpoint(constructor) { const className = constructor.name; const endpointPath = "/" + className.toLowerCase(); httpEndpoints[endpointPath] = new constructor(); } @registerEndpoint class Families { // implementation... } @registerEndpoint class Castles { // implementation... } console.log(httpEndpoints) // {"/families": Families, "/castles": Castles} httpEndpoints["/families"].get() // ["Lannister", "Targaryen"]
To send parameters to our decorator, we need to transform it from a regular decorator to a decorator factory. A decorator factory is a function that returns a decorator. To make one, all we need is to wrap our original decorator, like this:
const httpEndpoints = {}; function registerEndpointFactory(endpointPath) { return function registerEndpoint(constructor) { httpEndpoints[endpointPath] = new constructor(); } } @registerEndpointFactory("/families/stark/members") class StarkMembers { private members = ["Robb", "Sansa", "Arya"]; get() { return this.members; } @protect post(request) { this.members.push(request.body); } } console.log(httpEndpoints) // {"/families/stark/members": StarkMembers} httpEndpoints["/families/stark/members"].get() // ["Robb", "Sansa", "Arya"]
Now we can also wrap our protect
decorator, so it will get the expected token
as a parameter:
function protect(token) { return function(target, propertyKey, descriptor) { const originalFunction = descriptor.value; descriptor.value = function(request) { if (request.token !== token) { throw new Error("forbiden!"); } const bindedOriginalFunction = originalFunction.bind(this); const result = bindedOriginalFunction(request); return result; }; return descriptor; }; } class StarkMembers { private members = ["Robb", "Sansa", "Arya"]; @protect("abc") post(request) { this.members.push(request.body); } }
Conclusion
At this point you have a good understanding of decorators, how they work, as well as the power and expressiveness they allow us as programmers. In this article I covered the most useful and common techniques, however, there’s much more to learn. Decorators in Typescript can be applied not only to classes and methods, but also to properties of classes, and method arguments. I hope that with your newly acquired understanding of decorators you’ll be able to pick it up easily from the documentation. As always, I put all of this article’s code in the playground so go ahead and play!
yay, you did it!
If you’ve enjoyed this article feel free to clap for it so more people will find it and enjoy it as well, and if you wish to see more of the stuff I write you’re welcome to follow me. Let me know in the comments if you have any questions.
Also, you might want to check out my other TypeScript articles about generics and augmentation or about classes design, or my latest article about how cyber professionals avoid getting hacked!