Recently, InjectionToken
has gained popularity for its versatility, especially when combined with the inject
function, which simplifies the consumption of InjectionToken
and adds type safety.
Despite these advantages, using InjectionToken
can still present challenges in terms of developer experience (DX). In this post, we’ll dive into these challenges and demonstrate how createInjectionToken
from ngxtension
offers solutions to enhance your Angular development workflow.
Understanding InjectionToken
Creates a token that can be used in a DI Provider.
While the Angular Documentation provides a formal definition, in simpler terms, we can describe InjectionToken
as the most basic unit that can participate in Angular’s Dependency Injection (DI) system. Unlike traditional class-based providers, InjectionToken
can be as minimal as a string
, a number
, or an array etc., serving as a versatile and compact building block for DI configurations.
We can create an InjectionToken
by creating a new instance of the InjectionToken
class
Next, we can provide values for this token
Finally, we can consume this token
Note that Angular DI is hierarchical so
inject(BASE_URL)
will retrieve the value from the closestInjector
that provides a value forBASE_URL
Challenges with InjectionToken
In its basic form, InjectionToken
suffices for the needs of most Angular users. However, some users encounter challenges when dealing with more advanced use cases, making it difficult to promote widespread adoption of these advanced InjectionToken
features.
1. Type-safe provider
The initial challenge we encounter is not an advanced use case; rather, it’s primarily a concern related to DX. Let’s review the process of providing a value for an InjectionToken
.
In this example, the useValue
property has the potential to accept virtually any value, even when TOKEN
is explicitly defined as an InjectionToken<string>
. For instance, we can inadvertently provide a number
as the value for TOKEN
:
To compound the issue further, we might inadvertently assign a value that shares some of its APIs with a string
, such as an Array
:
In this last scenario, the subtlety of the issue may go unnoticed until unexpected behavior surfaces in the UI.
However, there is a straightforward solution at hand. We can create a custom provide
function (no pun intended!) that allows consumers to supply the intended value type:
With this approach, consumers now have a clear and type-safe way to provide values for BASE_URL
:
By using the provideBaseUrl
function, we ensure that only values of the correct type are accepted, effectively eliminating the type-safety issue.
2. Did I mention type-safety?
We’ve previously highlighted that useValue
lacks strong typing, and this limitation extends to other providers like useFactory
as well.
To address this issue, we can employ our custom provide function:
With this custom function, providing values for BASE_URL
becomes more type-safe:
However, the real challenge arises from the varying providers that Angular’s DI can accept, such as
useFactory
withdeps
.Provider
interface will not enforce the dependencies for auseFactory
multi
token. Handling themulti
token use-case is tricky because theInjectionToken
type differs from the value provider type
Adapting our custom provide function to handle these different cases can become a constant task.
3. Different ways to achieve the same thing
The InjectionToken
provides more flexibility than just using new InjectionToken(description)
. Specifically, it allows for the use of a factory function, which can change how the InjectionToken
is consumed.
When a factory function is provided, it introduces two key characteristics to the token:
BASE_URL
is provided in the Root Injector by default (i.e:providedIn: 'root'
). This effectively makesBASE_URL
a Tree-shakable Token- We can consume
BASE_URL
without the need to explicitly provide a value for it if we utilize DI in the factory function to access other dynamic values.
A few months back, I penned a (hot take) blog post on using InjectionToken
as a Service and have since implemented this approach in various libraries. Throughout this journey, I’ve experienced the challenges of utilizing InjectionToken
and, more notably, explaining its advanced usages to others.
Enter createInjectionToken
To simplify the usage of nearly all InjectionToken
instances across my projects, I’ve created a utility called createInjectionToken
.
createInjectionToken
is a function that takes a factory function and additional options as parameters. The result of calling createInjectionToken
is a tuple [injectFn, provideFn, TOKEN]
, offering both convenience in usage and flexibility for consumers to rename these components as needed.
1. Creating a root token
By default, createInjectionToken
creates a root token. The consumers can immediately retrieve the value by calling injectFn
2. Creating a non-root token
To create a non-root token, pass isRoot: false
to the 2nd argument of createInjectionToken
. Now, the consumers need to provide the value for the token by invoking provideFn()
before they can retrieve the value with injectFn()
injectFn
As the name suggests, injectFn
is a CIF (Custom Inject Function) for effortlessly accessing the token’s value:
Furthermore, injectFn
offers a high degree of type-safety by accepting an InjectOptions
object, enabling precise control over the Resolution Modifier:
Importantly, injectFn
is equipped to handle the subtleties of the Injection Context by accepting an optional Injector
:
This comprehensive functionality enhances both usability and type safety.
3. Creating a Token with Dependencies
You can create a token that depends on other tokens by simply configuring the factory function to accept specific arguments and providing those dependencies through the strongly-typed deps
option. The rules of Hierarchical Dependency Injection still apply in this context.
When isRoot
is set to false
, consumers must first provide the value using provideFn()
before they can access it with injectFn()
.
This approach allows you to create tokens with dependencies while maintaining strong typing and adherence to the principles of Hierarchical Dependency Injection.
4. Creating a multi
Token
By setting multi
to true
, we’ve defined a multi
token. When multi
is enabled, the behavior of injectFn
and provideFn
undergoes a slight change.
It’s important to highlight that a multi
token will automatically set isRoot
to false
.
createInjectionToken
is made publicly available via ngxtension. Check the documentation for more details.
Conclusion
In conclusion, we’ve embarked on a journey to unravel the intricacies of Angular’s Dependency Injection system and explore how InjectionToken
can sometimes pose type-safety and developer experience challenges. Along the way, we’ve introduced a valuable tool in the form of createInjectionToken
, a utility that simplifies the process of creating and consuming tokens while enhancing type safety.
Check out ngxtension for more cool utilities. Until next time 👋