Decorators and Reflector
Let's start with the obvious — TypeScript syntax is slightly different from JavaScript syntax because it has static typing capabilities. During the compilation of TypeScript code into JavaScript, the compiler can provide additional JavaScript code that can be used to obtain information about static types.
Let's experiment a bit. Create a file src/app/services.ts
in the ditsmod/rest-starter repository, and paste the following code into it:
class Service1 {}
class Service2 {
constructor(service1: Service1) {}
}
As you can see, in the constructor of Service2
, a static data type is specified for the service1
parameter. If you run the command:
npm run build
TypeScript code will be compiled and placed into the dist/app/services.js
file. It will look like this:
class Service1 {
}
class Service2 {
constructor(service1) { }
}
That is, the information about the parameter type in the Service2
constructor is lost. But if we use a class decorator, the TypeScript compiler will output more JavaScript code containing information about static typing. For example, let’s use the injectable
decorator:
import { injectable } from '@ditsmod/core';
class Service1 {}
@injectable()
class Service2 {
constructor(service1: Service1) {}
}
Now, using the npm run build
command, the TypeScript compiler converts this code into the following JavaScript code and inserts it into dist/app/services.js
:
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 __metadata = (this && this.__metadata) || function (k, v) {
if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v);
};
import { injectable } from '@ditsmod/core';
class Service1 {
}
let Service2 = class Service2 {
constructor(service1) { }
};
Service2 = __decorate([
injectable(),
__metadata("design:paramtypes", [Service1])
], Service2);
Fortunately, you will rarely need to inspect the dist
folder and analyze compiled code, but it can sometimes be useful to glance at it for a general understanding of how static typing is transferred into JavaScript code. The most interesting part is found in the last four lines. It’s clear that the TypeScript compiler now associates the array [Service1]
with Service2
. This array contains information about the static parameter types detected by the compiler in the Service2
constructor.
Further analysis of the compiled code shows that the Reflect
class is used to store metadata with static typing. This class is assumed to be imported from the reflect-metadata library. The API of this library is then used by Ditsmod to read the above metadata. This process is handled by the so-called reflector.
Let’s see what tools Ditsmod provides for working with the reflector. Let’s make the previous example more complex to see how metadata can be extracted and how complex dependency chains can be formed. Consider three classes with the following dependency: Service3
-> Service2
-> Service1
. Insert the following code into src/app/services.ts
:
import { injectable, getDependencies } from '@ditsmod/core';
class Service1 {}
@injectable()
class Service2 {
constructor(service1: Service1) {}
}
@injectable()
class Service3 {
constructor(service2: Service2) {}
}
console.log(getDependencies(Service3)); // [ { token: [class Service2], required: true } ]
The getDependencies()
function uses the reflector and returns an array of direct dependencies of Service3
. You might guess that by passing Service2
to getDependencies()
, we’ll see the dependency on Service1
. This way, you can automatically build the entire dependency chain Service3
-> Service2
-> Service1
. This process in DI is called "dependency resolution". And here the word "automatically" is intentionally bolded because it is a very important feature supported by DI. Users only pass Service3
to DI, and they don’t need to manually explore what this class depends on — DI can resolve the dependency automatically. By the way, users will rarely need to use the getDependencies()
function, except in a few rare cases.
Strictly speaking, the mechanism of storing and retrieving metadata from the reflector using decorators is not yet Dependency Injection. However, Dependency Injection extensively uses decorators and the reflector in its operation, so in this documentation, you might sometimes see that DI "obtains information about class dependencies" although in reality, it’s the reflector that does this.
The code in the last example can be compiled and run with the following command:
tput reset && npm run build && node dist/app/services.js
To have the code automatically execute after every change, you can use two terminals. In the first terminal, you can run the command to compile the code:
npm run build -- --watch
And in the second terminal, you can run the command to execute the compiled code:
node --watch dist/app/services.js
Now, if in src/app/services.ts
you pass Service1
to the getDependencies()
function, after a few seconds, you should see the output [ { token: [class Service1], required: true } ]
in the second terminal.