Advanced Dependency Injection in Angular

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

FeatureDescriptionUse Case
Multi-ProvidersProvide multiple values for the same tokenPlugins, interceptors, multiple loggers
Hierarchical InjectorsChild injectors override or extend parent servicesComponent-scoped service instances
@Optional()Dependency is optional, inject null if missingServices not always present
@Self()Inject only from local injector, no upward lookupEnforce strict injector locality
@Self() + @Optional()Optional service only if provided locallyConditional 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.


Comments

No comments yet. Why don’t you start the discussion?

Leave a Reply

Your email address will not be published. Required fields are marked *