Search code examples
angulardependency-injectionmoduleinterfacecollision-detection

What if you want name collision for Angular InjectionToken?


Goal

I want to write a feature module that consumes a service. This feature module should not be concerned with the implementation of this service, but rather expect that it implement an interface and that's it.

Caveats and Corollaries

Caveat: The interface that the service should implement will not be defined by this module.

Corollary: Where should this interface be defined?

No provider for InjectionToken

The feature module is meant to be used in many applications across our organization. Each is able to implement the interface for the required service in a different way. If a new use case presents itself, the interface may change and prompt changes to the feature module.

The design that we settled on placed this interface into a shared library.

This article shows that using an InjectionToken causes the following error 'NullInjectorError: No provider for InjectionToken ...`

I've created a StackBlitz example that illustrates the above setup and issue.

Research

The above thoughtram article describes how OpaqueToken was designed to prevent accidental collision of these strings. Adding that, later InjectionToken wrapped the OpaqueToken to add additional typescript support.

I would like the typescript support for me and my team, but with a token that allows controlled collision.

Minimum Reproducible Example

See this StackBlitz Controlled Collision Token

Sub-optimal Solution

The above example can be fixed if the InjectionToken is replaced with a string.

Change the following two (identical files) as such

  • /app/external-feature/lib/shared/i-shared.service.ts
  • /app/internal-adapter/shared/i-shared.service.ts
export const I_SHARED_SERVICE = new InjectionToken<ISharedService>('i-shared-service.shared');
=> 
export const I_SHARED_SERVICE = 'i-shared-service.shared';

The result is a loss of typescript support. This seems like a step backwards.


Solution

  • It takes a village to raise a child or to come up with a good architecture.

    After discussion with my coworker I've realized that an interface module (a separate npm package) solves the problem.

    Result

    The result is that I've got my consuming projects, my feature project and the interface project.

    consuming project: provides adapter between feature module and internal consumer, providing interface project's InjectionToken.

    feature project: provides feature components and consumes interface project's InjectionToken.

    interface project provides interfaces corresponding InjectionTokens