Angular DI: the genetic tokens pitfall
Tags: angular, typescript, webdev
Published: 2023-09-15 10:04:55

## 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.
```typescript
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
```typescript
@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.


![Type checking error](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/z88ssrip1pmzo34fexjp.png)

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

```typescript
@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**?

``` typescript
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


![Unexpected runtime error](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/p19udox5sk95be8n6wxq.png)

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. 

```typescript
providers: [
  {
    provide: PaymentServiceToken,
    useClass: PaymentService // <--- be extremely cautious here
  }
],
```

Moreover, there is an open [issue in the Angular repo](https://github.com/angular/angular/issues/33883) 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. 

```typescript
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
```typescript
function getPaymentService(): (new (...args: any) => IPaymentService) {
  return environment.production 
    ? PaymentService
    : MockPaymentService
}
```

Or rely on Angular's **Type** [interface](https://angular.io/api/core/Type)
```typescript
function getPaymentService(): Type<IPaymentService> {
  return environment.production
    ? PaymentService
    : MockPaymentService
}
```

This solution will throw a compilation error if I mistakenly provide a wrong service


![Compilation error for a wrong service](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/vmkr87j8tstqsua1i4lt.png)

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.