home/blog/engineering
NestJS logo with a cat in the background
Profile picture of Josh Console
Josh Console Assoc. Director of Engineering
Posted on Feb 4, 2022

NestJS Dependency Injection with Abstract Classes

#typescript#nestjs

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 AnimalModule
21
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 AnimalModule
18
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 AnimalModule
22
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!

© EF Education First