Published: April 15, 2021
Introduction
Dependency Injection (DI) is one of Angular’s core features that enables loose coupling and efficient service management. While the basics of DI are straightforward, Angular’s DI system is incredibly powerful and flexible — supporting advanced concepts like multi-providers, hierarchical injectors, and decorators like @Optional()
and @Self()
.
In this post, we’ll explore these advanced DI features with concrete examples, helping you write cleaner, more maintainable Angular applications.
1. Multi-Providers: Registering Multiple Instances for a Single Token
Sometimes, you want to provide multiple implementations for the same injection token — for example, a list of plugins or handlers.
Angular supports this with multi-providers by setting multi: true
in the provider config.
Example: Multi-Providers for Logging Services
export interface Logger {
log(message: string): void;
}
@Injectable()
export class ConsoleLogger implements Logger {
log(message: string) {
console.log('ConsoleLogger:', message);
}
}
@Injectable()
export class AlertLogger implements Logger {
log(message: string) {
alert('AlertLogger: ' + message);
}
}
@NgModule({
providers: [
{ provide: Logger, useClass: ConsoleLogger, multi: true },
{ provide: Logger, useClass: AlertLogger, multi: true }
]
})
export class AppModule {}
Injecting the Multi-Providers
@Component({...})
export class AppComponent {
constructor(@Inject(Logger) private loggers: Logger[]) {
this.loggers.forEach(logger => logger.log('Hello from multi-provider!'));
}
}
Here, Angular injects an array of all provided Logger
instances.
2. Hierarchical Injectors: Parent-Child Injector Scopes
Angular’s DI system uses a hierarchical injector tree matching the component tree. This allows child components to override or extend services scoped to parents.
Example: Overriding a Service in Child Component
@Injectable()
export class UserService {
getName() { return 'Global User'; }
}
@Component({
selector: 'app-parent',
template: `<app-child></app-child>`,
providers: [UserService]
})
export class ParentComponent {}
@Component({
selector: 'app-child',
template: `Hello {{userName}}!`,
providers: [{ provide: UserService, useClass: ChildUserService }]
})
export class ChildComponent {
userName: string;
constructor(private userService: UserService) {
this.userName = this.userService.getName();
}
}
@Injectable()
export class ChildUserService extends UserService {
getName() { return 'Child User'; }
}
Here, ChildComponent
injects its own scoped UserService
instance, overriding the parent’s version.
3. @Optional()
: Inject a Dependency Only If Available
Sometimes a service may or may not be present in the injector hierarchy. The @Optional()
decorator prevents Angular from throwing an error if the service is missing, instead injecting null
.
Example: Using @Optional()
@Component({...})
export class ProfileComponent {
constructor(@Optional() private analyticsService: AnalyticsService) {
if (analyticsService) {
analyticsService.track('Profile Viewed');
} else {
console.warn('AnalyticsService not available');
}
}
}
If AnalyticsService
is not provided anywhere, Angular injects null
and the component handles it gracefully.
4. @Self()
: Restrict Injection to the Local Injector
By default, Angular traverses the injector tree upward to find a dependency. The @Self()
decorator forces Angular to look only in the local injector (usually the current component or module).
Example: Using @Self()
@Component({
selector: 'app-child',
template: `Child Component`,
providers: [{ provide: UserService, useClass: ChildUserService }]
})
export class ChildComponent {
constructor(@Self() private userService: UserService) {
console.log(userService.getName());
}
}
If the UserService
isn’t provided locally, Angular throws an error.
5. Combining @Optional()
and @Self()
You can combine these decorators to inject a service only if it is locally available, otherwise receive null
.
Example:
@Component({...})
export class SettingsComponent {
constructor(@Optional() @Self() private configService: ConfigService) {
if (configService) {
console.log('Config found:', configService.getConfig());
} else {
console.log('No local ConfigService provided');
}
}
}
Summary Table
Feature | Description | Use Case |
---|---|---|
Multi-Providers | Provide multiple values for the same token | Plugins, interceptors, multiple loggers |
Hierarchical Injectors | Child injectors override or extend parent services | Component-scoped service instances |
@Optional() | Dependency is optional, inject null if missing | Services not always present |
@Self() | Inject only from local injector, no upward lookup | Enforce strict injector locality |
@Self() + @Optional() | Optional service only if provided locally | Conditional local service injection |
Final Thoughts
Mastering Angular’s advanced DI features unlocks powerful patterns — from plugin architectures to fine-grained control over service scopes.
Experiment with these concepts to write modular, scalable Angular apps.