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
1const BASE_URL = new InjectionToken<string>("Base URL");Next, we can provide values for this token
1// application wise2bootstrapApplication(AppComponent, {3 providers: [{ provide: BASE_URL, useValue: "http://base.url" }],4});5
6// route (sub-tree)7const routes: Routes = [8 {9 path: "",10 providers: [{ provide: BASE_URL, useValue: "http://route-base.url" }],11 },12];13
14// component/directive15@Component({16 providers: [{ provide: BASE_URL, useValue: "http://component-base.url" }],17})18export class MyCmp {}Finally, we can consume this token
1@Component({})2export class MyCmp {3 baseUrl = inject(BASE_URL); // string4}Note that Angular DI is hierarchical so
inject(BASE_URL)will retrieve the value from the closestInjectorthat 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.
1{ provide: BASE_URL, useValue: 'the value' }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:
1{ provide: BASE_URL, useValue: 234 }To compound the issue further, we might inadvertently assign a value that shares some of its APIs with a string, such as an Array:
1{ provide: BASE_URL, useValue: ['hi', 'world'] }2
3const baseUrl = inject(BASE_URL);4baseUrl.slice(0, 1);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:
1export const BASE_URL = new InjectionToken<string>("Base URL");2
3// 👇 Enforce value type constraints4export function provideBaseUrl(value: string) {5 return { provide: BASE_URL, useValue: value };6}With this approach, consumers now have a clear and type-safe way to provide values for BASE_URL:
1bootstrapApplication(AppComponent, {2 providers: [3 provideBaseUrl("http://base.url"),4 provideBaseUrl(123), // a value other than `string` will surface as a compilation error5 ],6});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.
1{ provide: BASE_URL, useFactory: () => ['we', 'can', 'do', 'whatever']}To address this issue, we can employ our custom provide function:
1export function provideBaseUrl(url: string | (() => string)) {2 return {3 provide: BASE_URL,4 useFactory: typeof url === "function" ? url : () => url,5 };6}With this custom function, providing values for BASE_URL becomes more type-safe:
1provideBaseUrl("http://raw.value.url");2provideBaseUrl(() => {3 // Note: We can utilize DI in here via `inject()`4 return "http://factory.value.url";5});However, the real challenge arises from the varying providers that Angular’s DI can accept, such as
useFactorywithdeps.Providerinterface will not enforce the dependencies for auseFactorymultitoken. Handling themultitoken use-case is tricky because theInjectionTokentype 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.
1export const BASE_URL = new InjectionToken("Base URL", {2 factory: () => {3 return "http://default.url";4 },5});When a factory function is provided, it introduces two key characteristics to the token:
BASE_URLis provided in the Root Injector by default (i.e:providedIn: 'root'). This effectively makesBASE_URLa Tree-shakable Token- We can consume
BASE_URLwithout 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
1export const [injectFn, provideFn, TOKEN] = createInjectionToken(2 () => "root factory",3);By default, createInjectionToken creates a root token. The consumers can immediately retrieve the value by calling injectFn
1export class MyCmp {2 value = injectFn(); // string3}2. Creating a non-root token
1export const [injectFn, provideFn, TOKEN] = createInjectionToken(2 () => "non root factory",3 { isRoot: false },4);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()
1@Component({2 // 👇 automatically use the factory function3 providers: [provideFn()],4})5export class MyCmp {6 value = injectFn(); // string7}injectFn
As the name suggests, injectFn is a CIF (Custom Inject Function) for effortlessly accessing the token’s value:
1injectFn(); // stringFurthermore, injectFn offers a high degree of type-safety by accepting an InjectOptions object, enabling precise control over the Resolution Modifier:
1export class MyCmp {2 value = injectFn({ self: true }); // string3 parentValue = injectFn({ skipSelf: true, optional: true }); // string | null4}Importantly, injectFn is equipped to handle the subtleties of the Injection Context by accepting an optional Injector:
1export class MyCmp {2 injector = inject(Injector);3
4 ngOnInit() {5 const baseUrl = injectFn({ injector: this.injector }); // Functions seamlessly, returning a string6 }7}This comprehensive functionality enhances both usability and type safety.
3. Creating a Token with Dependencies
1export const [injectDep, provideDep, DEP] = createInjectionToken(() => 1);2
3export const [injectFn, provideFn, TOKEN] = createInjectionToken(4 (dep: number) => dep * 2,5 // 👇 This is strongly typed based on the parameters of the factory function6 { deps: [DEP] },7 // { deps: [] }, // Compilation error8 // { deps: [OTHER_DEP_THAT_IS_NOT_NUMBER] }, // Compilation error9 // { deps: [DEP, SOME_OTHER_DEP] }, // Compilation error10);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.
1export class MyCmp {2 value = injectFn(); // 2 (1 * 2)3}When isRoot is set to false, consumers must first provide the value using provideFn() before they can access it with injectFn().
1@Component({2 providers: [provideDep(5), provideFn()],3})4export class MyCmp {5 value = injectFn(); // 106}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
1export const [injectLocales, provideLocale] = createInjectionToken(() => "en", {2 multi: true,3});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.
1@Component({2 providers: [3 provideLocale(), // Provides the first value using the factory function4 provideLocale("es"), // Provides the second value. Note that it accepts a string instead of a string[]5 provideLocale(() => "fr"), // Provides the third value using a factory function6 ],7})8export class MyCmp {9 locales = injectLocales(); // ['en', 'es', 'fr']10}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 👋