@lido-nestjs/di
v1.0.1
Published
NestJS Dependency Injection runtime helpers for Lido Finance projects. Part of [Lido NestJS Modules](https://github.com/lidofinance/lido-nestjs-modules/#readme)
Downloads
4,002
Readme
Nest JS Dependency Injection runtime helpers
NestJS Dependency Injection runtime helpers for Lido Finance projects. Part of Lido NestJS Modules
Install
yarn add @lido-nestjs/di
Motivation
This module exists to solve following things:
- the lack of runtime interfaces in Typescript
- necessity of
@Inject
decorators for constructor arguments when working with NestJS - use of
Symbol
to point to specific implementation when working with NestJS
The problem
When working with NestJS in case you need two different implementations of one interface, you have to introduce multiple tokens (symbols), which point to different implementations.
// bar-service.interface.ts
export interface BarServiceInterface {
someMethod(): string;
}
// bar-service-a.ts
export const BarServiceAToken = Symbol();
export class BarServiceA implements BarServiceInterface {
someMethod(): string {
return 'BarServiceA';
}
}
// bar-service-b.ts
export const BarServiceBToken = Symbol();
export class BarServiceB implements BarServiceInterface {
someMethod(): string {
return 'BarServiceB';
}
}
// foo-service.ts
import { Inject, Injectable } from '@nestjs/common';
import { BarServiceInterface } from './bar-service.interface';
import { BarServiceAToken } from './bar-service-a';
import { BarServiceBToken } from './bar-service-b';
@Injectable()
export class FooService {
// it's necessary to inject `BarServiceAToken` symbol here to point to specific implementation
public constructor(
@Inject(BarServiceAToken) private barService: BarServiceInterface,
) {}
public checkInstanceOf() {
// this is impossible because Typescript types do not exist in runtime
this.barService instanceof BarServiceInterface;
}
}
Solution
Module exposes two primitives:
createInterface<I>(nameOfInterface: string): InterfaceTag<I>
function.
Creates special interface-like anonymous class InterfaceTag
that acts like an interface
utilizing Symbol.hasInstance
method that allows to override behavior of instanceof
operator.
@ImplementsAtRuntime<T>(interfaceTag: InterfaceTag<T>): ClassDecorator
class decorator
Class decorator indicating that class implements interface at runtime with the help of Reflect
.
Needed for proper work of instanceof
operator for class instances.
Usage
Basic usage
import { createInterface, ImplementsAtRuntime } from '@lido-nestjs/di';
interface FooInterface {
foo(): string;
}
interface BarInterface {
bar(): number;
}
const FooInterface = createInterface<FooInterface>('FooInterface');
const BarInterface = createInterface<BarInterface>('BarInterface');
@ImplementsAtRuntime(FooInterface)
export class FooBar implements FooInterface, BarInterface {
bar(): number {
return 2;
}
foo(): string {
return 'bar';
}
}
const foobar = new FooBar();
console.log(foobar instanceof FooInterface === true); // true
console.log(foobar instanceof BarInterface === true); // true
Nest.js usage
// service.interface.ts
import { createInterface } from '@lido-nestjs/di';
export interface ServiceInterface {
doSmth(): string;
}
// the interface name and the name of the constant should be the same
export const ServiceInterface = createInterface<ServiceInterface>('ServiceInterface');
// service.ts
import { ImplementsAtRuntime } from '@lido-nestjs/di';
import { Injectable } from '@nestjs/common';
// here, we are importing the type and the constant in one variable 'ServiceInterface'
import { ServiceInterface } from './service.interface';
@Injectable()
@ImplementsAtRuntime(ServiceInterface)
export class ServiceA implements ServiceInterface {
doSmth(): string {
return 'serviceA';
}
}
@Injectable()
@ImplementsAtRuntime(ServiceInterface)
export class ServiceB implements ServiceInterface {
doSmth(): string {
return 'serviceB';
}
}
// service.module.ts
import { Injectable } from '@nestjs/common';
import { ServiceA, ServiceB } from './service';
import { ServiceInterface } from './service.interface';
export interface ServiceModuleOptions {
service: 'A' | 'B';
}
@Module({})
export class ServiceModule {
static forRoot(options?: ServiceModuleOptions): DynamicModule {
return {
global: true,
...this.forFeature(options),
};
}
static forFeature(options?: ServiceModuleOptions): DynamicModule {
return {
module: ServiceModule,
// depending on the options module can provide
// different immplementation for `ServiceInterface`
providers: [
options?.service === 'A'
? {
provide: ServiceInterface,
useClass: ServiceA,
}
: {
provide: ServiceInterface,
useClass: ServiceB,
},
],
exports: [ServiceInterface],
};
}
}
// some-other-service.ts
export class SomeOtherService {
// no `@Inject` decorator here
public constructor(private service: ServiceInterface) {}
public someMethod() {
// implementation of doSmth depends on `ServiceModuleOptions` value
this.service.doSmth();
}
}