Dependency Injection
Decorators and reflector
Let's start with the obvious — TypeScript syntax partially differs from JavaScript syntax because it provides capabilities for static typing. JavaScript has no static typing at all, but during compilation of TypeScript code to JavaScript the compiler can emit additional JavaScript code that can be used to obtain information about a given static type.
Let's do a quick experiment. In the following code the constructor of Service2
specifies a static type for the parameter service1
:
class Service1 {}
class Service2 {
constructor(service1: Service1) {}
}
After compilation this code will look like:
class Service1 {
}
class Service2 {
constructor(service1) { }
}
That is, the information about the parameter type in the constructor of Service2
is lost. But if we configure the TypeScript compiler in a certain way and use a class decorator, the TypeScript compiler will emit more JavaScript code with information about the static typing. For example, let's use the injectable
decorator:
import { injectable } from '@ditsmod/core';
class Service1 {}
@injectable()
class Service2 {
constructor(service1: Service1) {}
}
Now the TypeScript compiler transforms this code into the following JavaScript code:
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 analyze compiled code, but to get a general idea of the mechanism of transferring static typing into JavaScript code, it can sometimes be useful to look at it. The most interesting part is located in the last four lines. Obviously, the TypeScript compiler now associates the array [Service1]
with Service2
. This array is the information about the static types of the parameters that the compiler found in the constructor of Service2
.
Further analysis of the compiled code indicates that the Reflect
class is used to preserve metadata about static typing. It is assumed that this class is imported from the reflect-metadata library. The API of that library is then used by Ditsmod to read the metadata shown above. This is handled by the so-called reflector.
Let's see what tools Ditsmod has for working with the reflector. Let's complicate the previous example to see how metadata can be extracted and complex dependency chains formed. Consider three classes with the following dependency chain Service3
-> Service2
-> Service1
:
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 the array of direct dependencies of Service3
. You probably guess that if you pass Service2
to getDependencies()
you'll see the dependency on Service1
. Thus you can automatically compose the whole dependency chain Service3
-> Service2
-> Service1
. This process in DI is called "dependency resolution". And the word "automatically" is purposely bolded here because it's a very important feature supported by DI. Users pass only Service3
to the DI, and they don't need to manually investigate what this class depends on — DI can resolve the dependency automatically. By the way, even the getDependencies()
function itself will rarely be used by users, except in some rare special cases.
Strictly speaking, the mechanism of saving and obtaining metadata from the reflector via decorators is not Dependency Injection. But Dependency Injection heavily uses decorators and the reflector in its operation, so sometimes this documentation may say that DI receives information about a class's dependencies, while in fact the reflector is responsible for that.