Nest JS Dependency Injection runtime helpers
NestJS Dependency Injection runtime helpers for Catalist Finance projects.
Part of Catalist NestJS Modules
Install
yarn add @catalist-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.
export interface BarServiceInterface {
someMethod(): string;
}
export const BarServiceAToken = Symbol();
export class BarServiceA implements BarServiceInterface {
someMethod(): string {
return 'BarServiceA';
}
}
export const BarServiceBToken = Symbol();
export class BarServiceB implements BarServiceInterface {
someMethod(): string {
return 'BarServiceB';
}
}
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 {
public constructor(
@Inject(BarServiceAToken) private barService: BarServiceInterface,
) {}
public checkInstanceOf() {
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 '@catalist-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);
console.log(foobar instanceof BarInterface === true);
Nest.js usage
import { createInterface } from '@catalist-nestjs/di';
export interface ServiceInterface {
doSmth(): string;
}
export const ServiceInterface =
createInterface<ServiceInterface>('ServiceInterface');
import { ImplementsAtRuntime } from '@catalist-nestjs/di';
import { Injectable } from '@nestjs/common';
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';
}
}
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,
providers: [
options?.service === 'A'
? {
provide: ServiceInterface,
useClass: ServiceA,
}
: {
provide: ServiceInterface,
useClass: ServiceB,
},
],
exports: [ServiceInterface],
};
}
}
export class SomeOtherService {
public constructor(private service: ServiceInterface) {}
public someMethod() {
this.service.doSmth();
}
}