Angular DI: the genetic tokens pitfall
Mikhail Istomin
Posted on September 15, 2023
Intro
Recently I have discovered that using generic tokens for DI in Angular has an unexpected problem. It's not a critical issue, but rather something to be aware of.
In a nutshell, Angular and Typescript are not able to verify that a provided class really implements an interface, specified for a token.
App example
I declare the DI token PaymentServiceToken and provide the implementation on the module level.
export interface IPaymentService{
getPaymentTypes(): void;
authorizePayment(): void;
}
export const PaymentServiceToken =
new InjectionToken<IPaymentService>('Payment Service token')
@NgModule({
...
providers: [
{
provide: PaymentServiceToken,
useClass: PaymentService
}
],
})
export class AppModule { }
Note that the token is initialised as a generic class to let the consumers know that the implementations for this token will implement the interface IPaymentService.
Then the service can be consumed by a component via DI like this
@Component({...})
export class PaymentComponent implements OnInit{
constructor(
@Inject(PaymentServiceToken) private paymentService:IPaymentService,
) {}
// OR
// private paymentService = inject(PaymentServiceToken);
ngOnInit(): void {
this.paymentService.getPaymentTypes();
}
}
As a result I expect interaction with the injected service to be type-safe. In case of service misuse the errors should be thrown in compilation time.
At the same time I can provide other implementations for the same token on the module level without making changes in the components level. Something like
@NgModule({
...
providers: [
{
provide: PaymentServiceToken,
// useClass: PaymentService
useClass: environment.production
? PaymentService
: MockPaymentService
}
],
})
export class AppModule { }
This approach is useful for some cases:
- replace a real service with a mock version to avoid API calls
- replace an old service with a new version having some extra features or refactoring
- switching between implementations depending on configs or feature flags
Problem
To enable type checking I set up the token using the IPaymentService interface. But what if, by mistake, I provide the implementation that doesn't implement the interface?
export const PaymentServiceToken =
new InjectionToken<IPaymentService>('Payment Service token');
// DeliveryService doesn't implement IPaymentService
class DeliveryService(){}
@NgModule({
providers: [
{
provide: PaymentServiceToken,
// useClass: PaymentService
useClass: DeliveryService // <--- wrong service !!!
}
],
})
export class AppModule { }
The result is a disaster. I have no errors in compile time. But in runtime the TypeError is thrown and the app is ruined
Providing the wrong service is for sure the developer's fault, but Typescript and Angular failed to detect it during compilation.
Solution
Unfortunately, I haven't found a way to arrange proper type checking to verify that the token's implementation meets the token's interface.
providers: [
{
provide: PaymentServiceToken,
useClass: PaymentService // <--- be extremely cautious here
}
],
Moreover, there is an open issue in the Angular repo connected to the generic token's problem. Looks like there is no quick and simple solution.
The only thing I can suggest is to define a function with typing which verifies that the returned class implements the certain interface.
function getPaymentService(): (new () => IPaymentService) {
return environment.production
? PaymentService
: MockPaymentService
}
@NgModule({
providers: [
{
provide: PaymentServiceToken,
useClass: getPaymentService()
}
],
})
export class AppModule { }
We can improve the function getPaymentService to make it suitable for handling services with DI in constructors
function getPaymentService(): (new (...args: any) => IPaymentService) {
return environment.production
? PaymentService
: MockPaymentService
}
Or rely on Angular's Type interface
function getPaymentService(): Type<IPaymentService> {
return environment.production
? PaymentService
: MockPaymentService
}
This solution will throw a compilation error if I mistakenly provide a wrong service
Honestly, I don't like this solution since you have to write such functions manually for each generic token. I just can't come up with something better than that.
I hope the more useful solution will be introduced in the next versions of Angular.
Posted on September 15, 2023
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.