This article was also posted on eftech.com
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:
/* IAnimal.ts */
export interface IAnimal {
speak(): string;
}
/* animal.service.ts */
export class Dog implements IAnimal {
speak() {
return "Woof";
}
}
/* animal.module.ts */
@Module({
providers: [{
provide: "IAnimal",
useClass: Dog,
}]
})
export class AnimalModule
/* client.ts */
export class Client {
constructor(@Inject("IAnimal") private animal: IAnimal) {}
// Later...
this.animal.speak();
}
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:
/* IAnimal.ts */
export interface IAnimal {
speak(): string;
}
export const IAnimalToken = "IAnimal";
// or slightly better...
export const IAnimalToken = Symbol("IAnimal");
/* animal.module.ts */
@Module({
providers: [{
provide: IAnimalToken,
useClass: Dog,
}]
})
export class AnimalModule
/* client.ts */
export class Client {
constructor(@Inject(IAnimalToken) private animal: IAnimal) {}
// Later...
this.animal.speak();
}
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!
/* IAnimal.ts */
export abstract class IAnimal {
abstract speak(): string;
}
/* animal.service.ts */
// You can implement an abstract class without extending it!
export class Dog implements IAnimal {
speak() {
return "Woof";
}
}
/* animal.module.ts */
@Module({
providers: [{
provide: IAnimal,
useClass: Dog,
}]
})
export class AnimalModule
/* client.ts */
export class Client {
constructor(private animal: IAnimal) {}
// Later...
this.animal.speak();
}
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!
Top comments (4)
I’m searching around for best practice of Nestjs and IoC. Above example will still leak the Dog class as extra import, while we only need the interface. It’s really a half-baked solution comparing to Java or C#.
This is super super super useful. I had no idea that we were able to implement abstract classes instead of extending them. Thank you very much for sharing!
Great
And what if I need several animals?