/ TypeScript

TypeScript Decorators: JavaScript Foundation

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

  1. Decorator Introduction
  2. JavaScript Foundation
  3. Reflection
  4. Parameter Decorators
  5. Property Decorators
  6. Method Decorators
  7. 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
{
"compilerOptions": {
"target": "ESNext",
"experimentalDecorators": true,
"emitDecoratorMetadata": false
},
"include": ["./**/*.ts"]
}

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) {
var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
return c > 3 && r && Object.defineProperty(target, key, r), r;
};

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(
target: any,
propertyKey: string | symbol,
descriptor: PropertyDescriptor,
) {
descriptor.enumerable = true;
return descriptor;
}

class Demo {
@Enumerable
public foo() {
// do nothing
}
}

const demo = new Demo();
$ 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) {
var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
return c > 3 && r && Object.defineProperty(target, key, r), r;
};
function Enumerable(target, propertyKey, descriptor) {
descriptor.enumerable = true;
return descriptor;
}
class Demo {
foo() {
// do nothing
}
}
__decorate([
Enumerable
], Demo.prototype, "foo", null);
const demo = new Demo();

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
// Punctuation and spacing added to improve readability
// Original licensed under https://github.com/Microsoft/TypeScript/blob/master/LICENSE.txt
var __decorate = (
(this && this.__decorate)
||
function(decorators, target, key, desc) {
var c = arguments.length,
r = (
c < 3
? target
: (
desc === null
? desc = Object.getOwnPropertyDescriptor(target, key)
: desc
)
),
d;
if (
typeof Reflect === "object"
&&
typeof Reflect.decorate === "function"
) {
r = Reflect.decorate(decorators, target, key, desc);
} else {
for (var i = decorators.length - 1; i >= 0; i--) {
if (d = decorators[i]) {
r = (
(
c < 3
? d(r)
: (
c > 3
? d(target, key, r)
: d(target, key)
)
)
||
r
);
}
}
}
return (
c > 3
&&
r
&&
Object.defineProperty(target, key, r),
r
);
}
);
  • 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

    1. target: base object
    2. propertyKey: name or symbol of the active object
    3. descriptor: 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].
  • 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 when Reflect.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 be target.
    • With more than three arguments, the decorator is called with r as the property descriptor (in addition to target and propertyKey)
    • If there are three exactly, the decorator is called without anything to connect it to the current state (just target and propertyKey).
  • The return checks to see if the descriptor has been updated. If there are more than three arguments, the decorator was called with r, so it might have changed. If r is defined and the target is able to define propertyKey with the r-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(
target: any,
propertyKey: string | symbol,
parameterIndex: number,
) {
console.log(target);
console.log(propertyKey);
console.log(parameterIndex);
}

class ParameterExample {
public logThis(
first: string = "",
@LogParameter greeting: string = "Hello, world",
) {
// do nothing
}
}

const demoParameter = new ParameterExample();
$ 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) {
var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
return c > 3 && r && Object.defineProperty(target, key, r), r;
};
var __param = (this && this.__param) || function (paramIndex, decorator) {
return function (target, key) { decorator(target, key, paramIndex); }
};
function LogParameter(target, propertyKey, parameterIndex) {
console.log(target);
console.log(propertyKey);
console.log(parameterIndex);
}
class ParameterExample {
logThis(first = "", greeting = "Hello, world") {
// do nothing
}
}
__decorate([
__param(1, LogParameter)
], ParameterExample.prototype, "logThis", null);
const demoParameter = new ParameterExample();

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) {
return function (target, key) { decorator(target, key, paramIndex); }
};

__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(
target: any,
propertyKey: string | symbol,
) {
console.log(target);
console.log(propertyKey);
}

class PropertyExample {
@LogProperty
public greeting: string;

constructor() {
this.greeting = "Hello, world";
}
}

const demoExample = new PropertyExample();
$ 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) {
var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
return c > 3 && r && Object.defineProperty(target, key, r), r;
};
function LogProperty(target, propertyKey) {
console.log(target);
console.log(propertyKey);
}
class PropertyExample {
constructor() {
this.greeting = "Hello, world";
}
}
__decorate([
LogProperty
], PropertyExample.prototype, "greeting", void 0);
const demoExample = new PropertyExample();

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(
target: any,
propertyKey: string | symbol,
descriptor: PropertyDescriptor,
) {
console.log(target);
console.log(propertyKey);
console.log(descriptor);
}

class MethodExample {
@LogMethod
public foo() {
// do nothing
}
}

const demoMethod = new MethodExample();
$ 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) {
var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
return c > 3 && r && Object.defineProperty(target, key, r), r;
};
function LogMethod(target, propertyKey, descriptor) {
console.log(target);
console.log(propertyKey);
console.log(descriptor);
}
class MethodExample {
foo() {
// do nothing
}
}
__decorate([
LogMethod
], MethodExample.prototype, "foo", null);
const demoMethod = new MethodExample();

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) {
console.log(target.constructor.name);
}

@LogClass
class ClassExample {
// do nothing
}

const demoClass = new ClassExample();
$ 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) {
var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
return c > 3 && r && Object.defineProperty(target, key, r), r;
};
function LogClass(target) {
console.log(target.constructor.name);
}
let ClassExample = class ClassExample {
};
ClassExample = __decorate([
LogClass
], ClassExample);
const demoClass = new ClassExample();

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.

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.

CJ Harries

I did a thing once. Change "blog." to "cj@" and you've got my email. All these opinions are mine and might not be shared by clients or employers.

Read More