NestJS Dependency Injection with Abstract Classes
A seemingly common complaint regarding NestJS and TypeScript is the absence of interfaces in runtime code (since interfaces are removed during transpilation). This limitation prevents the use of constructor-based dependency injection and instead necessitates use of the @Inject
decorator. That decorator requires us to associate some "real" JavaScript token with the interface to resolve the dependency - often just a string of the interface name:
1/* IAnimal.ts */2export interface IAnimal {3 speak(): string;4}5
6/* animal.service.ts */7export class Dog implements IAnimal {8 speak() {9 return "Woof";10 }11}12
13/* animal.module.ts */14@Module({15 providers: [{16 provide: "IAnimal",17 useClass: Dog,18 }]19})20export class AnimalModule21
22/* client.ts */23export class Client {24 constructor(@Inject("IAnimal") private animal: IAnimal) {}25
26 // Later...27 this.animal.speak();28}
This approach is adequate, but the "IAnimal" magic string is a little fragile and not actually associated with the interface. A modest improvement can be made by exporting a token with the interface:
1/* IAnimal.ts */2export interface IAnimal {3 speak(): string;4}5
6export const IAnimalToken = "IAnimal";7// or slightly better...8export const IAnimalToken = Symbol("IAnimal");9
10/* animal.module.ts */11@Module({12 providers: [{13 provide: IAnimalToken,14 useClass: Dog,15 }]16})17export class AnimalModule18
19/* client.ts */20export class Client {21 constructor(@Inject(IAnimalToken) private animal: IAnimal) {}22
23 // Later...24 this.animal.speak();25}
At least now the token reference is consistent. Still, manually injecting tokens like this can be cumbersome and conceptually unsatisfying. What we would really like is a true JavaScript object in the place of the interface, which we can reference universally at runtime. What we would really like is...an abstract class!
1/* IAnimal.ts */2export abstract class IAnimal {3 abstract speak(): string;4}5
6/* animal.service.ts */7// You can implement an abstract class without extending it!8export class Dog implements IAnimal {9 speak() {10 return "Woof";11 }12}13
14/* animal.module.ts */15@Module({16 providers: [{17 provide: IAnimal,18 useClass: Dog,19 }]20})21export class AnimalModule22
23/* client.ts */24export class Client {25 constructor(private animal: IAnimal) {}26
27 // Later...28 this.animal.speak();29}
Now a single, "real" JavaScript reference is used throughout the application to resolve the dependency. Whether or not the minor inconvenience of token-based dependency injection justifies this usage of abstract classes is a separate question. If you're feeling lazy, though, this is a convenient option!