TypeScript frameworks like Angular and NestJS use dependency injection where they resolve TypeScript type hints into injection tokens and use those tokens to fetch and inject dependencies into constructors at runtime:
@Injectable() // <-- registers this class in the container
class X {
}
class Y {
constructor(private x: X) // <-- this is detected to need class X
{
}
}
Since the TypeScript type annotation compiles away into JavaScript, where private x: X
is just x
, how exactly do they inspect the type of the constructor arguments at runtime? The answer is probably that either the TypeScript compiler leaves behind some information for them somewhere, or that these frameworks do source code analysis during the build.
Could someone explain how exactly this is conventionally done in TypeScript?
CodePudding user response:
So let's take a look at a nest new
project and look at the AppController
import { Controller, Get } from '@nestjs/common';
import { AppService } from './app.service';
@Controller()
export class AppController {
constructor(private readonly appService: AppService) {}
@Get()
getHello(): string {
return this.appService.getHello();
}
}
(I know this uses @Controller()
, but it'll be the same for @Injectable()
, at least for the scope of this question)
Now let's take a look at it's compiled output, dist/app.controller.js
"use strict";
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);
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.AppController = void 0;
const common_1 = require("@nestjs/common");
const app_service_1 = require("./app.service");
let AppController = class AppController {
constructor(appService) {
this.appService = appService;
}
getHello() {
return this.appService.getHello();
}
};
__decorate([
(0, common_1.Get)(),
__metadata("design:type", Function),
__metadata("design:paramtypes", []),
__metadata("design:returntype", String)
], AppController.prototype, "getHello", null);
AppController = __decorate([
(0, common_1.Controller)(),
__metadata("design:paramtypes", [app_service_1.AppService])
], AppController);
exports.AppController = AppController;
//# sourceMappingURL=app.controller.js.map
Lot's of stuff going on in the top of the file, defining __deccorate
methods and such. Let's look at it actually being used for the @Controller()
decorator
AppController = __decorate([
(0, common_1.Controller)(),
__metadata("design:paramtypes", [app_service_1.AppService])
], AppController);
This "design:paramtypes'
is what Nest is reading during application start and knows what to inject where. So Nest would see this and find AppService
, so then it would read the metadata of AppService
and do so recursively until it can instantiate the service, then put the provider into a metadata map and check that each time it finds metadata to see if the service already exists in the context of the module. If so, it won't recreate the service. Otherwise, it'll work it's way through the metadata and injection tree and create each service.
side note: you can see the types for the
@Get()
decorator and the related parameters too. Nest makes use of some of this in pipes, just FYI.
The really important thing here is that that __decorate()
method and related metadata doesn't get emitted by Typescript unless there is at least one decorator on the class, hence why we use @Inejctable()
. If the class doesn't inject anything, then technically you don't need the @Injectable()
, but it's convention to keep it around for uniformity.