This post looks at how TypeScript compiles decorators. It pulls the raw JavaScript from the compiler and breaks down the result. It has basic decorator examples of each type to examine the JavaScript output.
The Series so Far
- Decorator Introduction
- JavaScript Foundation
- Reflection
- Parameter Decorators
- Property Decorators
- Method Decorators
- Class Decorators
Eventual Topics:
- Where Decorators Work
- Decorating Instance Elements vs. Static Elements
- Examples
- Pairing Parameter Decorators with Method Decorators
- Pairing Property Decorators with Class Decorators
Code
You can view the code related to this post under the post-02-javascript-foundation tag.
Why Look at the JavaScript?
A little bit of perspective is never a bad thing. I often forget that JavaScript is somewhere in the toolchain because ts-node keeps me so far removed. Looking at how the compiler handles decorators will shed some light on the process and make debugging the inevitable issues easier.
Configuration
I’ll be using this tsconfig.json throughout the post.
tsconfig.json | |
1 2 3 4 5 6 7 8 | { |
From the Source
Decorators begin with stored, prebuilt JavaScript. The decorateHelper, deep in the compiler, exports the __decorate function wherever it needs to go. The same function is used for all decorator types.
Raw
As of v2.7.2, decorateHelper generates this JavaScript.
decorateHelper | |
1 2 3 4 5 6 | var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) { |
To verify, we can create a simple class, decorate it, and see how TypeScript compiles it.
main.ts | |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 | function Enumerable( |
$ tsc --project tsconfig.json |
main.js | |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 | var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) { |
The __decorate blob is defined at the top and consumed at the bottom with foo as an input. If you need more examples, either keep reading or compile more things.
Prettified and Polished
As it stands, __decorate isn’t easy to grok. Let’s clean it up a bit to see how it works.
decorate.js | |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 | // Pulled from https://github.com/Microsoft/TypeScript/blob/v2.7.2/src/compiler/transformers/ts.ts#L3577 |
Line 5 is a guarded assignment; it reuses an existing
__decorateor builds it from scratch.Line 8 counts the call’s arguments. Remember the arguments have typically been
target: base objectpropertyKey: name or symbol of the active objectdescriptor: the active property descriptor
We can reasonably infer having all three is important.
Lines 10-16 set the initial item that will decorated.
- If there are fewer than three arguments, the item is the
target, which should be the class. - If there are three (or more) arguments, the item is the property descriptor for
target[propertyKey].
- If there are fewer than three arguments, the item is the
Lines 19-24 search the Reflect object for a
decoratemethod. I scratched my head over this for a few minutes, then discovered a great SO answer. It’s future planning for the day whenReflect.decoratedoes exist.Lines 26-42 loop over the passed-in decorators and attempt to evaluate them.
- Once again, three arguments is important. If there are fewer than three, the decorator is called with
r, which as we learned above, should betarget. - With more than three arguments, the decorator is called with
ras the property descriptor (in addition totargetandpropertyKey) - If there are three exactly, the decorator is called without anything to connect it to the current state (just
targetandpropertyKey).
- Once again, three arguments is important. If there are fewer than three, the decorator is called with
The
returnchecks to see if the descriptor has been updated. If there are more than three arguments, the decorator was called withr, so it might have changed. Ifris defined and thetargetis able to definepropertyKeywith ther-descriptor, the object will be updated.ris always returned.
Analysis
To keep with the JavaScript theme, I’m going to look at each kind of decorator and the JS it generates. This is just a cursory overview; when I wrap back around with posts about the individual decorators I’ll go deeper with more examples and more complicated setups.
Because I didn’t do anything complicated with these decorators, I ended 3/4 with a fairly pessimistic assessment. I assure you that will change once I bring factories and fancy config back into the mix. Vanilla decorators are fantastic at monitoring state and not much else.
Parameter Decorators
To explore parameter decorators, let’s build an uncomplicated logger.
main.ts | |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 | function LogParameter( |
$ tsc --project tsconfig.json |
main.js | |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 | var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) { |
The __decorate call is full of __param calls. That’s a new function. Like __decorate, __param is stored deep in the compiler.
paramHelper | |
1 2 3 | var __param = (this && this.__param) || function (paramIndex, decorator) { |
__param is only used by parameter decorators, unlike __decorate, which is used by all. Like __decorate, __param’s assignment is guarded. When created, __param becomes a factory that takes target and propertyKey as input with fixed decorator and paramIndex.
Returning to line 10, after the __decorate and __param declarations, we see a tidier LogParameter and ParameterExample. All of the TS syntactic sugar has been removed for a faster, vanilla JS experience.
We’re mainly interested in the __decorate call itself. On line 21, the decorators array has been filled with __param calls. This converts the unique signature of parameter decorators into the standard (target, propertyKey, descriptor) format, albeit without a descriptor. Similarly, the decoration is happening on logThis (which owns the parameter) without a descriptor.
All of this together means parameter decorators really don’t do much for us. We can verify a parameter has been used. We don’t have access to the value it was used with. Returns from parameter decorators are ignored which means any changes we attempt will persist beyond this decorator. All that being said, there are some very good uses for the limited access we have, which will be explored more in the Parameter Decorator post (TODO!).
Property Decorators
Once again, building a simple, logging decorator is a good way to explore.
main.ts | |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 | function LogProperty( |
$ tsc --project tsconfig.json |
main.js | |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 | var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) { |
Below __decorate’s declaration and the simplified core logic, __decorate’s call cements how limited the property decorator appears. LogProperty isn’t called with a property descriptor so any modifications it makes will persist beyond the decorator. __decorate’s final argument, void 0, reiterates that. Once again, __decorate has left us with solid observation options.
Method Decorators
Logging decorators are very easy to write.
main.ts | |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 | function LogMethod( |
$ tsc --project tsconfig.json |
main.js | |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 | var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) { |
Now we’re getting somewhere. Method decorators provide a descriptor and update target[propertyKey] with changes made to descriptor that are returned. While the __decorate call ends with a null, as we saw above, __decorate should pull the proper property descriptor with a null tail.
To be fair, there’s not a whole lot we can streamline with access to the property descriptor. Any changes made on anything but the descriptor will persist. We do, as always, have some fantastic observation options via __decorate, and the code is getting easier to read.
Class Decorators
There’s not much to logging a class name.
main.ts | |
1 2 3 4 5 6 7 8 9 10 | function LogClass(target: any) { |
$ tsc --project tsconfig.json |
main.js | |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 | var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) { |
Class decorators can directly affect the classes they decorate by modifying their return, so you won’t hear me complaining about this one. The compiled result is the simplest to read, which is an added bonus.
Recap
TypeScript builds all the decorators from the stored __decorate code. __decorate is used by the all the decorators; __param pops up with parameter decorators to transform their odd signature into something useful. Logging decorators are very easy to code. Without frills, parameter and property decorators are useful to monitor application flow. Method and class decorators can make simple changes without too much trouble.
Legal
The TS logo is a modified @typescript avatar; I turned the PNG into a vector. I couldn’t find the original and I didn’t see any licensing on the other art, so it’s most likely covered by the TypeScript project’s Apache 2.0 license. Any code from the TS project is similarly licensed.
If there’s a problem with anything, my email’s in the footer. Stay awesome.
