Tout ce que vous injectez n’est pas une classe. Une URL d’API, une configuration, un ensemble de feature flags, une fonction, une liste de plugins : tout cela a besoin d’un token. InjectionToken<T> est la clé typée sous laquelle vous les fournissez et les demandez, et les fonctions provideX() sont la manière moderne d’exposer la configuration d’une fonctionnalité (exactement comme provideRouter() ou provideHttpClient()).
InjectionToken
import { InjectionToken, inject } from '@angular/core';
// Un token typé pour une valeur qui n’est PAS une classe (config, fonction, string, objet)
export const API_URL = new InjectionToken<string>('API_URL');
// Avec une valeur par défaut : fonctionne même si personne ne le fournit
export const PAGE_SIZE = new InjectionToken<number>('PAGE_SIZE', {
providedIn: 'root',
factory: () => 20,
});
// Utilisation
export class UsersApi {
private readonly baseUrl = inject(API_URL);
private readonly pageSize = inject(PAGE_SIZE);
}Fonctions provideX()
// Le pattern provideX() : l’API publique d’une fonctionnalité ou d’une bibliothèque
export interface FeatureFlags {
newCheckout: boolean;
darkPromo: boolean;
}
const DEFAULT_FLAGS: FeatureFlags = { newCheckout: false, darkPromo: false };
export const FEATURE_FLAGS = new InjectionToken<FeatureFlags>('FEATURE_FLAGS', {
providedIn: 'root',
factory: () => DEFAULT_FLAGS,
});
export function provideFeatureFlags(flags: Partial<FeatureFlags>): EnvironmentProviders {
return makeEnvironmentProviders([
{ provide: FEATURE_FLAGS, useValue: { ...DEFAULT_FLAGS, ...flags } },
]);
}{"newCheckout":false,"darkPromo":false,"betaBadge":true}{"newCheckout":true,"darkPromo":true,"betaBadge":true}// Les EnvironmentProviders (makeEnvironmentProviders) ne s’utilisent QU’au niveau de
// l’application ou d’une route, pas dans les `providers` d’un composant. C’est voulu : c’est une « barrière »
// pour la configuration globale (routeur, http, animations, bibliothèques).
@Component({
// providers: [provideFeatureFlags(...)] ✗ erreur : EnvironmentProviders dans un composant
providers: [{ provide: FEATURE_FLAGS, useValue: { ...DEFAULT_FLAGS, newCheckout: true } }], // ✓
})
export class BetaZone {}multi: true — plugins
// Plugins : plusieurs providers pour le même token, regroupés dans un tableau
export const EXPORTERS = new InjectionToken<Exporter[]>('EXPORTERS');
providers: [
{ provide: EXPORTERS, useValue: csvExporter, multi: true },
{ provide: EXPORTERS, useValue: jsonExporter, multi: true },
{ provide: EXPORTERS, useClass: PdfExporter, multi: true },
];
// Le composant ignore combien de formats existent : il les reçoit tous
readonly exporters = inject(EXPORTERS); // Exporter[]Choisissez un format.
Initialiseurs
// Code à exécuter au démarrage, AVANT que l’application soit prête
export const appConfig: ApplicationConfig = {
providers: [
// remplace l’ancien APP_INITIALIZER ; peut renvoyer une Promise / un Observable
provideAppInitializer(() => inject(ConfigService).load()),
],
};
// Au niveau d’une route ou de l’« environnement » (p. ex. à la création de l’injecteur d’une fonctionnalité lazy)
{
path: 'admin',
providers: [provideEnvironmentInitializer(() => inject(AdminAnalytics).start())],
loadChildren: () => import('./admin/admin.routes'),
}Tokens optionnels
Parfois un token est vraiment optionnel, par exemple un tracker d’analytics que seuls certains environnements configurent. inject(TOKEN, { optional: true }) renvoie null au lieu de lever l’erreur NG0201. S’il existe une valeur par défaut raisonnable, mettez-la plutôt dans la factory, pour ne pas vérifier null partout.
export const ANALYTICS = new InjectionToken<Analytics>('ANALYTICS');
export class Checkout {
private readonly analytics = inject(ANALYTICS, { optional: true }); // null si personne ne l’a fourni
pay() {
this.analytics?.track('pay');
}
}Tokens légers dans les bibliothèques
Si un composant de bibliothèque cherche un enfant avec contentChild(LibHeader), la classe LibHeader se retrouve dans le bundle même dans les applications qui ne l’utilisent jamais. La solution de la documentation Angular : chercher une petite classe abstraite, que le vrai composant fournit lui-même sous ce token. Si personne n’utilise le composant, le bundler le supprime.
// le token : une classe abstraite vide, presque aucun code dans le bundle
export abstract class LibCardHeader {}
@Component({
selector: 'lib-card-header',
providers: [{ provide: LibCardHeader, useExisting: CardHeader }], // le vrai composant se fournit lui-même sous le token
template: '<ng-content />',
})
export class CardHeader extends LibCardHeader {}
@Component({ selector: 'lib-card', template: '...' })
export class Card {
// chercher par le token, pas par CardHeader : si personne n’utilise CardHeader, il quitte le bundle
protected readonly header = contentChild(LibCardHeader);
}Les tokens dans les tests
On remplace un token dans les tests exactement comme un service : on le fournit dans TestBed avec une autre valeur. On teste ainsi le même composant avec d’autres flags ou une autre URL d’API, sans toucher au code.
it('affiche le nouveau checkout quand le flag est activé', () => {
TestBed.configureTestingModule({
providers: [
{ provide: FEATURE_FLAGS, useValue: { newCheckout: true } },
{ provide: API_URL, useValue: 'http://localhost/test-api' },
],
});
const fixture = TestBed.createComponent(CheckoutPage);
fixture.detectChanges();
expect(fixture.nativeElement.textContent).toContain('Nouveau checkout');
});Que choisir
Class / @Service() → vous avez de la logique (méthodes), des dépendances, de l’état
InjectionToken<T> → valeurs : config, URLs, fonctions, constantes, interfaces
InjectionToken + multi → plugins / extensions (validateurs, exporters, interceptors)
provideX() → l’API de configuration publique d’une fonctionnalité / bibliothèqueLa hiérarchie dans laquelle Angular cherche un token : Providers et hiérarchie des injecteurs ; les tokens utilisés comme ports entre couches : Architecture hexagonale (ports et adapters).
Pièges fréquents
- Tokens de type string (
@Inject('API_URL')) : pas de types, collisions possibles. Utilisez toujoursInjectionToken. - Oublier
multi: truesur un provider → il écrase toute la liste au lieu de s’y ajouter. - Une interface comme token :
inject(MyInterface)ne fonctionne pas, car les interfaces n’existent pas à l’exécution. Utilisez un token ou une classe abstraite. - Des initialiseurs lents retardent le démarrage de l’application. Ne chargez que le strict nécessaire avant le premier rendu.
Aide-mémoire
new InjectionToken<T>('NAME') // quelqu’un doit le fournir
new InjectionToken<T>('NAME', { providedIn: 'root', factory }) // avec une valeur par défaut
{ provide: TOKEN, useValue: value }
{ provide: TOKEN, useFactory: () => create(inject(Other)) }
{ provide: TOKEN, useExisting: OtherToken }
{ provide: TOKEN, useClass: Impl, multi: true } // regroupés dans un tableau
inject(TOKEN) // NG0201 s’il manque
inject(TOKEN, { optional: true }) // null s’il manque
makeEnvironmentProviders([...]) // seulement dans app.config / les routes
provideAppInitializer(() => inject(Config).load()) // s’exécute avant le premier rendu