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
__decorate
or 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
decorate
method. I scratched my head over this for a few minutes, then discovered a great SO answer. It’s future planning for the day whenReflect.decorate
does 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
r
as the property descriptor (in addition totarget
andpropertyKey
) - If there are three exactly, the decorator is called without anything to connect it to the current state (just
target
andpropertyKey
).
- Once again, three arguments is important. If there are fewer than three, the decorator is called with
The
return
checks 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. Ifr
is defined and thetarget
is able to definepropertyKey
with ther
-descriptor, the object will be updated.r
is 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.