Dependency providers

From Get docs
Angular/docs/11/guide/dependency-injection-providers


Dependency providers

By configuring providers, you can make services available to the parts of your application that need them.

A dependency provider configures an injector with a DI token, which that injector uses to provide the runtime version of a dependency value.

Specifying a provider token

If you specify the service class as the provider token, the default behavior is for the injector to instantiate that class with new.

In the following example, the Logger class provides a Logger instance.

providers: [Logger]

You can, however, configure an injector with an alternative provider in order to deliver some other object that provides the needed logging functionality.

You can configure an injector with a service class, you can provide a substitute class, an object, or a factory function.

Dependency injection tokens

When you configure an injector with a provider, you are associating that provider with a dependency injection token, or DI token. The injector allows Angular to create a map of any internal dependencies. The DI token acts as a key to that map.

The dependency value is an instance, and the class type serves as a lookup key. Here, the injector uses the HeroService type as the token for looking up heroService.

heroService: HeroService;

When you define a constructor parameter with the HeroService class type, Angular knows to inject the service associated with that HeroService class token:

constructor(heroService: HeroService)

Though classes provide many dependency values, the expanded provide object lets you associate different kinds of providers with a DI token.

Defining providers

The class provider syntax is a shorthand expression that expands into a provider configuration, defined by the Provider interface. The following example is the class provider syntax for providing a Logger class in the providers array.

providers: [Logger]

Angular expands the providers value into a full provider object as follows.

[{ provide: Logger, useClass: Logger }]

The expanded provider configuration is an object literal with two properties:

  • The provide property holds the token that serves as the key for both locating a dependency value and configuring the injector.
  • The second property is a provider definition object, which tells the injector how to create the dependency value. The provider-definition key can be useClass, as in the example. It can also be useExisting, useValue, or useFactory. Each of these keys provides a different type of dependency, as discussed below.

Specifying an alternative class provider

Different classes can provide the same service. For example, the following code tells the injector to return a BetterLogger instance when the component asks for a logger using the Logger token.

[{ provide: Logger, useClass: BetterLogger }]

Configuring class providers with dependencies

If the alternative class providers have their own dependencies, specify both providers in the providers metadata property of the parent module or component.

[ UserService,
  { provide: Logger, useClass: EvenBetterLogger }]

In this example, EvenBetterLogger displays the user name in the log message. This logger gets the user from an injected UserService instance.

@Injectable()
export class EvenBetterLogger extends Logger {
  constructor(private userService: UserService) { super(); }

  log(message: string) {
    const name = this.userService.user.name;
    super.log(`Message to ${name}: ${message}`);
  }
}

The injector needs providers for both this new logging service and its dependent UserService.

Aliasing class providers

To alias a class provider, specify the alias and the class provider in the providers array with the useExisting property.

In the following example, the injector injects the singleton instance of NewLogger when the component asks for either the new or the old logger. In this way, OldLogger is an alias for NewLogger.

[ NewLogger,
  // Alias OldLogger w/ reference to NewLogger
  { provide: OldLogger, useExisting: NewLogger}]

Be sure you don't alias OldLogger to NewLogger with useClass, as this creates two different NewLogger instances.

Aliasing a class interface

Generally, writing variations of the same parent alias provider uses forwardRef as follows.

providers: [{ provide: Parent, useExisting: forwardRef(() => AlexComponent) }],

To streamline your code, you can extract that logic into a helper function using the provideParent() helper function.

// Helper method to provide the current component instance in the name of a `parentType`.
export function provideParent
  (component: any) {
    return { provide: Parent, useExisting: forwardRef(() => component) };
  }

Now you can add a parent provider to your components that's easier to read and understand.

providers:  [ provideParent(AliceComponent) ]

Aliasing multiple class interfaces

To alias multiple parent types, each with its own class interface token, configure provideParent() to accept more arguments.

Here's a revised version that defaults to parent but also accepts an optional second parameter for a different parent class interface.

// Helper method to provide the current component instance in the name of a `parentType`.
// The `parentType` defaults to `Parent` when omitting the second parameter.
export function provideParent
  (component: any, parentType?: any) {
    return { provide: parentType || Parent, useExisting: forwardRef(() => component) };
  }

Next, to use provideParent() with a different parent type, provide a second argument, here DifferentParent.

providers:  [ provideParent(BethComponent, DifferentParent) ]

Injecting an object

To inject an object, configure the injector with the useValue option. The following provider object uses the useValue key to associate the variable with the Logger token.

[{ provide: Logger, useValue: SilentLogger }]

In this example, SilentLogger is an object that fulfills the logger role.

// An object in the shape of the logger service
function silentLoggerFn() {}

export const SilentLogger = {
  logs: ['Silent logger says "Shhhhh!". Provided via "useValue"'],
  log: silentLoggerFn
};

Injecting a configuration object

A common use case for object literals is a configuration object. The following configuration object includes the title of the application and the address of a web API endpoint.

export const HERO_DI_CONFIG: AppConfig = {
  apiEndpoint: 'api.heroes.com',
  title: 'Dependency Injection'
};

To provide and inject the configuration object, specify the object in the @NgModule() providers array.

providers: [
  UserService,
  { provide: APP_CONFIG, useValue: HERO_DI_CONFIG }
],

Using an InjectionToken object

You can define and use an InjectionToken object for choosing a provider token for non-class dependencies. The following example defines a token, APP_CONFIG of the type InjectionToken.

import { InjectionToken } from '@angular/core';

export const APP_CONFIG = new InjectionToken<AppConfig>('app.config');

The optional type parameter, <AppConfig>, and the token description, app.config, specify the token's purpose.

Next, register the dependency provider in the component using the InjectionToken object of APP_CONFIG.

providers: [{ provide: APP_CONFIG, useValue: HERO_DI_CONFIG }]

Now you can inject the configuration object into the constructor with @Inject() parameter decorator.

constructor(@Inject(APP_CONFIG) config: AppConfig) {
  this.title = config.title;
}

Interfaces and dependency injection

Though the TypeScript AppConfig interface supports typing within the class, the AppConfig interface plays no role in dependency injection. In TypeScript, an interface is a design-time artifact, and doesn't have a runtime representation, or token, that the DI framework can use.

When the transpiler changes TypeScript to JavaScript, the interface disappears because JavaScript doesn't have interfaces.

Since there is no interface for Angular to find at runtime, the interface cannot be a token, nor can you inject it.

// Can't use interface as provider token
[{ provide: AppConfig, useValue: HERO_DI_CONFIG })]
// Can't inject using the interface as the parameter type
constructor(private config: AppConfig){ }

Using factory providers

To create a changeable, dependent value based on information unavailable before run time, you can use a factory provider.

In the following example, only authorized users should see secret heroes in the HeroService. Authorization can change during the course of a single application session, as when a different user logs in .

To keep security-sensitive information in UserService and out of HeroService, give the HeroService constructor a boolean flag to control display of secret heroes.

constructor(
  private logger: Logger,
  private isAuthorized: boolean) { }

getHeroes() {
  const auth = this.isAuthorized ? 'authorized ' : 'unauthorized';
  this.logger.log(`Getting heroes for ${auth} user.`);
  return HEROES.filter(hero => this.isAuthorized || !hero.isSecret);
}

To implement the isAuthorized flag, use a factory provider to create a new logger instance for HeroService.

const heroServiceFactory = (logger: Logger, userService: UserService) => {
  return new HeroService(logger, userService.user.isAuthorized);
};

The factory function has access to UserService. You inject both Logger and UserService into the factory provider so the injector can pass them along to the factory function.

export let heroServiceProvider =
  { provide: HeroService,
    useFactory: heroServiceFactory,
    deps: [Logger, UserService]
  };
  • The useFactory field specifies that the provider is a factory function whose implementation is heroServiceFactory.
  • The deps property is an array of provider tokens. The Logger and UserService classes serve as tokens for their own class providers. The injector resolves these tokens and injects the corresponding services into the matching heroServiceFactory factory function parameters.

Capturing the factory provider in the exported variable, heroServiceProvider, makes the factory provider reusable.

The following side-by-side example shows how heroServiceProvider replaces HeroService in the providers array.

import { Component } from '@angular/core';
import { heroServiceProvider } from './hero.service.provider';

@Component({
  selector: 'app-heroes',
  providers: [ heroServiceProvider ],
  template: `
    <h2>Heroes</h2>
    <app-hero-list></app-hero-list>
  `
})
export class HeroesComponent { }
import { Component } from '@angular/core';

import { HeroService } from './hero.service';

@Component({
  selector: 'app-heroes',
  providers: [ HeroService ],
  template: `
    <h2>Heroes</h2>
    <app-hero-list></app-hero-list>
  `
})
export class HeroesComponent { }

© 2010–2021 Google, Inc.
Licensed under the Creative Commons Attribution License 4.0.
https://v11.angular.io/guide/dependency-injection-providers