1
votes

Can anyone help me to understand DI Nest Fundamentals, my question:

"Is it possible to have a service class without @Injectable annotattion, and also this class does not belong to any module?" I saw on internet an example like below:

This class exists in a common folder:

export class NotificationService {
  constructor(
    @Inject(Logger) private readonly logger: LoggerService,
    private readonly appConfigService: AppConfigService,
    @Inject(HttpService) private readonly httpService: HttpService
  ) {}
 
  async sendNotification(msg: string) {
   ....
  } 
}

And then it was registered in another module in the the providers array:

import { Module, Logger, forwardRef, HttpModule } from '@nestjs/common';
import { MongooseModule } from '@nestjs/mongoose';
import { NotificationService } from '../../commons/notification/notification.service';
 
@Module({
    imports: [
        ...
    ],
    controllers: [InvoiceController],
    providers: [
        InvoiceService,
        NotificationService,
        Logger],
    exports: [InvoiceService]
})
export class InvoiceModule { }

Then it was injected in other service's constructor method

@Injectable()
export class InvoiceService {
 
    constructor(
        @Inject(Logger) private readonly logger: LoggerService,
        private readonly notificationService: NotificationService) { }
 
...
}

This works fine, but I don't know why. Why the notification service was injected correctly without add @Injectable, and without and import module?

1

1 Answers

3
votes

So let's break down what's really happening with the @Injectable() decorator.

Normally, we use decorators for setting metadata about the class, parameter, method, or property that we are decorating, or we are using it to somehow modify the method (if method decorator) or the property (property decorator) via descriptors. In the case of @Injectable() we aren't really doing either of these. Sure we're setting the scope metadata, but this doesn't really seem to set any metadata about "Hey, this class is injectable via the Nest framework". That's because what @Injectable() is really setting us up for is a special property of the tsconfig and tsc compiler, the emitDecoratorMetadata property. With this property, typescript will add on a bunch of extra functions at the beginning and end of the file. These functions look generally like this

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);
};

and

DelegatorService = __decorate([
    common_1.Injectable(),
    __metadata("design:paramtypes", [http_interceptor_service_1.HttpInterceptorService,
        websocket_interceptor_service_1.WebsocketInterceptorService,
        rpc_interceptor_service_1.RpcInterceptorService,
        gql_interceptor_service_1.GqlInterceptorService])
], DelegatorService);

This is the super important part, because this "design:paramtypes" is actually what Nest is reading when it comes determining what to inject.

This metadata is only available when a decorator is used anywhere in the class, and there was actually an eslint-typescript discussion about the metadata and how import type breaks it, along with a recent PR that really gets into the weeds of things.

I bring all of this up to say that because you have @Inject() in the constructor, the @Injectable() is actually extraneous, and not necessary unless you're going to be setting scope level metadata. The type metadat will alrady be emitted, so that explains why you don't need @Injectable() (though I still think it's a good idea to have it cause it provides clear intent).

Now for why the injection works properly, I promise this one is less complex: you added the NotificationsService to the InvoiceModule's providersarray. This tells Nest that any service inside ofInvoiceModulehas access toNotificationsService` so it can be injected here without a problem.