4. Advanced Injection-JS Features
You’ve mastered the fundamentals and hierarchies. Now, let’s explore some of the more advanced features of Injection-JS that allow for highly flexible and powerful dependency management in complex applications.
Multi-Providers (multi: true)
Sometimes, you don’t want to provide a single instance of a service, but rather multiple instances or values associated with a single injection token. This is where multi-providers come in handy. When multi: true is used in a provider, instead of replacing previous definitions for that token, it adds to a collection. When the token is resolved, you get an array of all registered values.
Common Use Cases:
- Registering multiple plugins for a specific extension point.
- Providing multiple validation rules for a form field.
- Aggregating event handlers or interceptors.
// src/app.ts (or a new file, e.g., multi-providers.ts)
import "reflect-metadata";
import { ReflectiveInjector, InjectionToken, Injectable, Inject } from 'injection-js';
// Define an InjectionToken for our "plugins"
export const PLUGIN_TOKEN = new InjectionToken<string>('PLUGIN_TOKEN');
// A service that will consume multiple plugins
@Injectable()
export class PluginHostService {
constructor(@Inject(PLUGIN_TOKEN) private plugins: string[]) {
console.log('PluginHostService initialized with plugins:', plugins);
}
runPlugins(): void {
if (this.plugins && this.plugins.length > 0) {
this.plugins.forEach(plugin => console.log(`Running plugin: ${plugin}`));
} else {
console.log("No plugins registered.");
}
}
}
// Define multiple providers for PLUGIN_TOKEN with multi: true
const providers = [
{ provide: PLUGIN_TOKEN, useValue: 'AuthPlugin', multi: true },
{ provide: PLUGIN_TOKEN, useValue: 'LoggingPlugin', multi: true },
{ provide: PLUGIN_TOKEN, useValue: 'CachingPlugin', multi: true },
PluginHostService // PluginHostService will receive an array of plugin names
];
const injector = ReflectiveInjector.resolveAndCreate(providers);
const pluginHost = injector.get(PluginHostService);
pluginHost.runPlugins();
// You can also get the array directly
const allPlugins = injector.get(PLUGIN_TOKEN);
console.log("Directly retrieved plugins:", allPlugins);
Output:
PluginHostService initialized with plugins: [ 'AuthPlugin', 'LoggingPlugin', 'CachingPlugin' ]
Running plugin: AuthPlugin
Running plugin: LoggingPlugin
Running plugin: CachingPlugin
Directly retrieved plugins: [ 'AuthPlugin', 'LoggingPlugin', 'CachingPlugin' ]
Notice how PluginHostService received an array ['AuthPlugin', 'LoggingPlugin', 'CachingPlugin'] because multi: true was used for each provider associated with PLUGIN_TOKEN.
Optional Dependencies (@Optional())
By default, if a service requests a dependency that the injector cannot resolve, it will throw an error. However, sometimes a dependency is not strictly required, and your service can function perfectly well without it. In such cases, you can mark the dependency as optional using the @Optional() decorator.
If an optional dependency cannot be resolved, the injector will provide null (or undefined depending on the TypeScript version and strictness) instead of throwing an error.
// src/app.ts (or optional-dependency.ts)
import "reflect-metadata";
import { ReflectiveInjector, Injectable, Optional, Inject } from 'injection-js';
// A simple service that might be optional
@Injectable()
export class DebugService {
log(message: string): void {
console.log(`[DEBUG] ${message}`);
}
}
// A service that may or may not use DebugService
@Injectable()
export class DataProcessorService {
constructor(@Optional() @Inject(DebugService) private debugService: DebugService | null) {
if (this.debugService) {
this.debugService.log('DataProcessorService initialized with DebugService.');
} else {
console.log('DataProcessorService initialized without DebugService.');
}
}
process(data: string): string {
this.debugService?.log(`Processing data: ${data}`);
return `Processed: ${data.toUpperCase()}`;
}
}
// Scenario 1: DebugService is provided
const providersWithDebug = [
DebugService,
DataProcessorService
];
const injectorWithDebug = ReflectiveInjector.resolveAndCreate(providersWithDebug);
const processorWithDebug = injectorWithDebug.get(DataProcessorService);
console.log(processorWithDebug.process("hello"));
console.log('\n--- Separator ---\n');
// Scenario 2: DebugService is NOT provided
const providersWithoutDebug = [
// No DebugService here
DataProcessorService
];
const injectorWithoutDebug = ReflectiveInjector.resolveAndCreate(providersWithoutDebug);
const processorWithoutDebug = injectorWithoutDebug.get(DataProcessorService);
console.log(processorWithoutDebug.process("world"));
Output:
[DEBUG] DataProcessorService initialized with DebugService.
[DEBUG] Processing data: hello
Processed: HELLO
--- Separator ---
DataProcessorService initialized without DebugService.
Processed: WORLD
As you can see, DataProcessorService adapts gracefully whether DebugService is available or not.
Host, Self, SkipSelf Decorators (Contextual Resolution)
These decorators provide fine-grained control over how injection-js searches for a dependency within an injector hierarchy. They are particularly useful in component-based architectures where you might want to specify where in the hierarchy a dependency should be resolved.
While their primary use is in Angular’s component tree, they apply equally to injection-js when you manually build complex injector hierarchies.
@Self(): Limits the search for a dependency to the current injector only. If the current injector doesn’t provide it, an error is thrown (unless@Optional()is also used). It does not look at the parent injector.// Example: A component needs a specific service from its own injector // @Injectable() // class MyComponent { // constructor(@Self() private myService: MyService) {} // }@Host(): This decorator is conceptually tied to the idea of a “host” element or component in a UI framework. In the context of a pure DI hierarchy, it means to search from the current injector up to its closest “host” parent, but not beyond the host. A “host” is often the injector where the component itself is defined or created. Forinjection-jsstandalone, it’s typically the injector that directly creates the instance of the class requesting the dependency. It implies searching up the direct parent chain until the injector that first provided the requesting class is found.// Example: A service needs a dependency provided by its immediate "parent" module, but not higher up. // @Injectable() // class ChildService { // constructor(@Host() private parentScopedService: ParentScopedService) {} // }@SkipSelf(): Starts the search for a dependency from the parent injector, skipping the current injector. This is useful when you want to ensure you get a higher-level instance of a service, even if the current injector also provides it (e.g., to access a global logger even if a local, specialized logger exists).// Example: A service needs the root logger, even if a local child logger exists. // @Injectable() // class MyService { // constructor(@SkipSelf() private globalLogger: Logger) {} // }
Demonstration with ConsoleWriter:
Let’s illustrate @Self() and @SkipSelf() with a concrete example.
// src/app.ts (or contextual-resolution.ts)
import "reflect-metadata";
import { ReflectiveInjector, Injectable, Inject, Optional, Self, SkipSelf } from 'injection-js';
@Injectable()
export class BaseService {
constructor() { console.log('BaseService created.'); }
identity = 'BaseService';
}
@Injectable()
export class FeatureService {
constructor() { console.log('FeatureService created.'); }
identity = 'FeatureService';
}
@Injectable()
export class RootComponent {
// BaseService provided at root
// FeatureService provided at root
constructor(
@Inject(BaseService) private base: BaseService,
@Inject(FeatureService) private feature: FeatureService
) {
console.log(`RootComponent gets BaseService: ${this.base.identity}`);
console.log(`RootComponent gets FeatureService: ${this.feature.identity}`);
}
}
@Injectable()
export class ChildComponent {
// Child injector provides its own FeatureService.
// We want to see how decorators change resolution.
constructor(
// 1. Get BaseService (provided by parent)
@Inject(BaseService) private inheritedBase: BaseService,
// 2. Get FeatureService from current injector only
@Self() @Inject(FeatureService) private localFeature: FeatureService,
// 3. Try to get a FeatureService from parent (skip self)
@SkipSelf() @Inject(FeatureService) private parentFeature: FeatureService,
// 4. Optionally get a service that might not exist locally or in parent
@Optional() @Inject('UNKNOWN_SERVICE') private unknownService: any | null
) {
console.log(`ChildComponent gets Inherited BaseService: ${this.inheritedBase.identity}`);
console.log(`ChildComponent gets Local FeatureService: ${this.localFeature.identity}`);
console.log(`ChildComponent gets Parent FeatureService (skipped self): ${this.parentFeature.identity}`);
console.log(`ChildComponent gets Unknown Service (optional): ${this.unknownService}`);
}
}
// --- Setup Injector Hierarchy ---
// Root Injector
const rootProviders = [
BaseService,
FeatureService, // Root FeatureService
RootComponent
];
const rootInjector = ReflectiveInjector.resolveAndCreate(rootProviders);
const rootComp = rootInjector.get(RootComponent);
// Child Injector
const childProviders = [
// Child provides its OWN FeatureService
{ provide: FeatureService, useClass: FeatureService },
ChildComponent
];
const childInjector = ReflectiveInjector.resolveAndCreate(childProviders, rootInjector);
console.log('\n--- Resolving from Child Injector ---');
const childComp = childInjector.get(ChildComponent);
// Verifying instances
console.log('\n--- Instance Verification ---');
console.log('Root BaseService instance:', rootInjector.get(BaseService).identity);
console.log('Root FeatureService instance:', rootInjector.get(FeatureService).identity);
console.log('ChildComponent\'s inherited BaseService === Root BaseService?',
childComp.inheritedBase === rootInjector.get(BaseService)); // true
console.log('ChildComponent\'s local FeatureService === Root FeatureService?',
childComp.localFeature === rootInjector.get(FeatureService)); // false, gets its own
console.log('ChildComponent\'s parent FeatureService === Root FeatureService?',
childComp.parentFeature === rootInjector.get(FeatureService)); // true, explicitly skipped self
Output Breakdown:
RootComponentgets instances ofBaseServiceandFeatureServicefrom therootInjector.ChildComponent’sinheritedBasegetsBaseServicefrom therootInjector(its parent), becauseBaseServiceis not provided inchildProviders.ChildComponent’slocalFeature(with@Self()) explicitly gets theFeatureServiceinstance provided inchildProviders, ignoring the parent.ChildComponent’sparentFeature(with@SkipSelf()) explicitly looks only in the parent chain, thus getting theFeatureServicefromrootInjector.unknownServiceisnullbecause it’s@Optional()and not provided.
This demonstrates how these decorators give you surgical precision over dependency resolution.
Dynamic Dependency Resolution
While constructor injection is the primary way to get dependencies, sometimes you need to dynamically resolve a dependency at runtime, outside of a constructor. For this, you can inject the Injector itself.
// src/app.ts (or dynamic-resolution.ts)
import "reflect-metadata";
import { ReflectiveInjector, Injectable, Injector } from 'injection-js';
@Injectable()
export class ExpensiveService {
constructor() { console.log("ExpensiveService created!"); }
doWork(): string { return "Work done by ExpensiveService"; }
}
@Injectable()
export class ServiceRequiringDynamicDependency {
constructor(private injector: Injector) {
// Injecting the Injector itself
console.log("ServiceRequiringDynamicDependency initialized.");
}
// This method might need ExpensiveService only under certain conditions
getExpensiveResult(): string {
if (Math.random() > 0.5) { // Simulate a condition
const expensiveService = this.injector.get(ExpensiveService); // Dynamic resolution
return expensiveService.doWork();
}
return "No expensive work needed right now.";
}
}
const providers = [
ExpensiveService,
ServiceRequiringDynamicDependency
];
const injector = ReflectiveInjector.resolveAndCreate(providers);
const myService = injector.get(ServiceRequiringDynamicDependency);
console.log(myService.getExpensiveResult());
console.log(myService.getExpensiveResult());
console.log(myService.getExpensiveResult());
In this example, ExpensiveService is only instantiated when getExpensiveResult() decides it’s needed, demonstrating dynamic resolution and potential lazy loading. However, be cautious: relying too heavily on injecting the Injector can obscure dependencies and make testing harder. Prefer constructor injection when possible.
Exercises / Mini-Challenges
Exercise 4.1: Building a Custom Validator Chain with Multi-Providers
Objective: Create a system where a form input can have multiple validation rules, and a validator host aggregates them using multi-providers.
Define an
InjectionTokenforValidator:// src/tokens/validator-token.ts import { InjectionToken } from 'injection-js'; export interface ValidatorFn { (value: string): string | null; // Returns error message or null if valid } export const VALIDATORS = new InjectionToken<ValidatorFn[]>('VALIDATORS');Create some
ValidatorFnimplementations:// src/validators/email-validator.ts import { ValidatorFn } from '../tokens/validator-token'; export const emailValidator: ValidatorFn = (value: string): string | null => { const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/; return emailRegex.test(value) ? null : 'Invalid email format'; }; // src/validators/required-validator.ts import { ValidatorFn } from '../tokens/validator-token'; export const requiredValidator: ValidatorFn = (value: string): string | null => { return value.trim().length > 0 ? null : 'Field is required'; }; // src/validators/min-length-validator.ts import { ValidatorFn } from '../tokens/validator-token'; export const minLengthValidator = (minLength: number): ValidatorFn => { return (value: string): string | null => { return value.length >= minLength ? null : `Minimum length is ${minLength}`; }; };Create a
ValidationServicethat consumesVALIDATORS:// src/services/validation.service.ts import { Injectable, Inject } from 'injection-js'; import { VALIDATORS, ValidatorFn } from '../tokens/validator-token'; @Injectable() export class ValidationService { constructor(@Inject(VALIDATORS) private validators: ValidatorFn[]) { console.log(`ValidationService initialized with ${validators.length} validators.`); } validate(value: string): string[] { const errors: string[] = []; this.validators.forEach(validator => { const error = validator(value); if (error) { errors.push(error); } }); return errors; } }In
app.ts(ormulti-validator.ts):- Set up an injector.
- Provide the
emailValidator,requiredValidator, and an instance ofminLengthValidator(5)usingmulti: trueforVALIDATORS. - Get
ValidationServiceand test different inputs.
// src/multi-validator.ts import "reflect-metadata"; import { ReflectiveInjector, InjectionToken, Injectable, Inject } from 'injection-js'; // --- Validator definitions (copy-paste or import from separate files) --- export interface ValidatorFn { (value: string): string | null; } export const VALIDATORS = new InjectionToken<ValidatorFn[]>('VALIDATORS'); export const emailValidator: ValidatorFn = (value: string): string | null => { const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/; return emailRegex.test(value) ? null : 'Invalid email format'; }; export const requiredValidator: ValidatorFn = (value: string): string | null => { return value.trim().length > 0 ? null : 'Field is required'; }; export const minLengthValidator = (minLength: number): ValidatorFn => { return (value: string): string | null => { return value.length >= minLength ? null : `Minimum length is ${minLength}`; }; }; @Injectable() export class ValidationService { constructor(@Inject(VALIDATORS) private validators: ValidatorFn[]) { console.log(`ValidationService initialized with ${validators.length} validators.`); } validate(value: string): string[] { const errors: string[] = []; this.validators.forEach(validator => { const error = validator(value); if (error) { errors.push(error); } }); return errors; } } // --- End Validator definitions --- const providers = [ { provide: VALIDATORS, useValue: requiredValidator, multi: true }, { provide: VALIDATORS, useValue: emailValidator, multi: true }, { provide: VALIDATORS, useValue: minLengthValidator(5), multi: true }, ValidationService ]; const injector = ReflectiveInjector.resolveAndCreate(providers); const validationService = injector.get(ValidationService); console.log('\nValidating "test@example.com":'); let errors = validationService.validate('test@example.com'); console.log(errors.length > 0 ? errors : 'No errors.'); console.log('\nValidating "abc":'); errors = validationService.validate('abc'); console.log(errors.length > 0 ? errors : 'No errors.'); console.log('\nValidating "":'); errors = validationService.validate(''); console.log(errors.length > 0 ? errors : 'No errors.');This exercise perfectly demonstrates
multi: truefor flexible, extensible validation.
Exercise 4.2: Feature Toggle with Optional Dependency
Objective: Implement a feature toggle where a specific “feature service” is only enabled if its provider is present, leveraging @Optional().
Create a
FeatureToggleService:// src/services/feature-toggle.service.ts import { Injectable } from 'injection-js'; @Injectable() export class FeatureToggleService { isFeatureEnabled(): boolean { console.log("FeatureToggleService: Feature is ENABLED."); return true; } }Create a
ComponentWithFeature:// src/components/component-with-feature.ts import { Injectable, Optional, Inject } from 'injection-js'; import { FeatureToggleService } from '../services/feature-toggle.service'; @Injectable() export class ComponentWithFeature { constructor( @Optional() @Inject(FeatureToggleService) private featureToggle: FeatureToggleService | null ) { if (this.featureToggle) { console.log("ComponentWithFeature: Initialized with feature toggle capability."); } else { console.log("ComponentWithFeature: Initialized WITHOUT feature toggle capability."); } } executeFeature(): string { if (this.featureToggle?.isFeatureEnabled()) { return "Feature logic executed!"; } else { return "Feature is not available or not enabled."; } } }In
app.ts(orfeature-toggle.ts):- Create an injector where
FeatureToggleServiceIS provided. GetComponentWithFeatureand execute its feature. - Create a separate injector where
FeatureToggleServiceis NOT provided. GetComponentWithFeaturefrom this injector and execute its feature.
// src/feature-toggle.ts import "reflect-metadata"; import { ReflectiveInjector, Injectable, Optional, Inject } from 'injection-js'; // --- Services & Component (copy-paste or import) --- @Injectable() export class FeatureToggleService { isFeatureEnabled(): boolean { console.log("FeatureToggleService: Feature is ENABLED."); return true; } } @Injectable() export class ComponentWithFeature { constructor( @Optional() @Inject(FeatureToggleService) private featureToggle: FeatureToggleService | null ) { if (this.featureToggle) { console.log("ComponentWithFeature: Initialized with feature toggle capability."); } else { console.log("ComponentWithFeature: Initialized WITHOUT feature toggle capability."); } } executeFeature(): string { if (this.featureToggle?.isFeatureEnabled()) { return "Feature logic executed!"; } else { return "Feature is not available or not enabled."; } } } // --- End Services & Component --- console.log("--- Scenario 1: Feature IS Enabled ---"); const enabledProviders = [ FeatureToggleService, ComponentWithFeature ]; const enabledInjector = ReflectiveInjector.resolveAndCreate(enabledProviders); const enabledComp = enabledInjector.get(ComponentWithFeature); console.log(enabledComp.executeFeature()); console.log("\n--- Scenario 2: Feature is DISABLED ---"); const disabledProviders = [ // FeatureToggleService is intentionally omitted ComponentWithFeature ]; const disabledInjector = ReflectiveInjector.resolveAndCreate(disabledProviders); const disabledComp = disabledInjector.get(ComponentWithFeature); console.log(disabledComp.executeFeature());This clearly shows how
@Optional()allows for flexible, configurable features based on provider presence.- Create an injector where
These advanced features, when used judiciously, unlock sophisticated dependency management patterns, making your applications more robust and adaptable.