Nicht alles, was du injizierst, ist eine Klasse. Eine API-URL, eine Konfiguration, ein Satz Feature Flags, eine Funktion, eine Liste von Plugins — sie alle brauchen ein Token. InjectionToken<T> ist der typisierte Schlüssel, unter dem du sie bereitstellst und anforderst, und die provideX()-Funktionen sind der moderne Weg, die Konfiguration eines Features bereitzustellen (genau wie provideRouter() oder provideHttpClient()).
InjectionToken
import { InjectionToken, inject } from '@angular/core';
// Ein typisiertes Token für einen Wert, der KEINE Klasse ist (Config, Funktion, String, Objekt)
export const API_URL = new InjectionToken<string>('API_URL');
// Mit Standardwert: funktioniert auch, wenn es niemand bereitstellt
export const PAGE_SIZE = new InjectionToken<number>('PAGE_SIZE', {
providedIn: 'root',
factory: () => 20,
});
// Verwendung
export class UsersApi {
private readonly baseUrl = inject(API_URL);
private readonly pageSize = inject(PAGE_SIZE);
}provideX()-Funktionen
// Das provideX()-Pattern: die öffentliche API eines Features oder einer Bibliothek
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}// EnvironmentProviders (makeEnvironmentProviders) dürfen NUR auf Anwendungs-
// oder Routenebene verwendet werden — nicht in den `providers` einer Komponente. Das ist Absicht: Es ist eine „Schranke“
// für globale Konfiguration (Router, HTTP, Animationen, Bibliotheken).
@Component({
// providers: [provideFeatureFlags(...)] ✗ Fehler: EnvironmentProviders in einer Komponente
providers: [{ provide: FEATURE_FLAGS, useValue: { ...DEFAULT_FLAGS, newCheckout: true } }], // ✓
})
export class BetaZone {}multi: true — Plugins
// Plugins: mehrere Provider für dasselbe Token, gesammelt in einem Array
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 },
];
// Die Komponente weiß nicht, wie viele Formate existieren — sie erhält alle
readonly exporters = inject(EXPORTERS); // Exporter[]Wähle ein Format.
Initializer
// Code, der beim Start laufen muss, BEVOR die Anwendung bereit ist
export const appConfig: ApplicationConfig = {
providers: [
// ersetzt den alten APP_INITIALIZER; kann ein Promise / Observable zurückgeben
provideAppInitializer(() => inject(ConfigService).load()),
],
};
// Auf Routen- oder „Environment“-Ebene (z. B. wenn der Injector eines Lazy Features erzeugt wird)
{
path: 'admin',
providers: [provideEnvironmentInitializer(() => inject(AdminAnalytics).start())],
loadChildren: () => import('./admin/admin.routes'),
}Optionale Tokens
Manchmal ist ein Token wirklich optional, etwa ein Analytics-Tracker, den nur manche Umgebungen konfigurieren. inject(TOKEN, { optional: true }) gibt null zurück, statt den Fehler NG0201 zu werfen. Gibt es einen sinnvollen Standardwert, gehört er besser in die factory, damit du nicht überall auf null prüfen musst.
export const ANALYTICS = new InjectionToken<Analytics>('ANALYTICS');
export class Checkout {
private readonly analytics = inject(ANALYTICS, { optional: true }); // null, wenn ihn niemand bereitgestellt hat
pay() {
this.analytics?.track('pay');
}
}Leichtgewichtige Tokens in Bibliotheken
Sucht eine Bibliothekskomponente ein Kind mit contentChild(LibHeader), landet die Klasse LibHeader im Bundle, auch in Apps, die sie nie verwenden. Die Lösung aus der Angular-Doku: Du suchst nach einer kleinen abstrakten Klasse, und die echte Komponente stellt sich selbst unter diesem Token bereit. Nutzt niemand die Komponente, entfernt der Bundler sie.
// das Token: eine leere abstrakte Klasse, fast kein Code im Bundle
export abstract class LibCardHeader {}
@Component({
selector: 'lib-card-header',
providers: [{ provide: LibCardHeader, useExisting: CardHeader }], // die echte Komponente stellt sich selbst unter dem Token bereit
template: '<ng-content />',
})
export class CardHeader extends LibCardHeader {}
@Component({ selector: 'lib-card', template: '...' })
export class Card {
// nach dem Token suchen, nicht nach CardHeader: nutzt niemand CardHeader, fliegt es aus dem Bundle
protected readonly header = contentChild(LibCardHeader);
}Tokens in Tests
Ein Token ersetzt du im Test genau wie einen Service: Du stellst ihn in TestBed mit einem anderen Wert bereit. So testest du dieselbe Komponente mit anderen Flags oder einer anderen API-URL, ohne den Code anzufassen.
it('zeigt den neuen Checkout, wenn das Flag an ist', () => {
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('Neuer Checkout');
});Was du wählst
Klasse / @Service() → du hast Logik (Methoden), Abhängigkeiten, Zustand
InjectionToken<T> → Werte: Config, URLs, Funktionen, Konstanten, Interfaces
InjectionToken + multi → Plugins / Erweiterungen (Validatoren, Exporter, Interceptors)
provideX() → die öffentliche Konfigurations-API für ein Feature / eine BibliothekDie Hierarchie, in der Angular ein Token sucht, beschreibt Provider & die Injector-Hierarchie; Tokens als Ports zwischen Schichten Hexagonale Architektur (Ports & Adapters).
Häufige Stolperfallen
- String-Tokens (
@Inject('API_URL')) — keine Typen, mögliche Kollisionen. Verwende immerInjectionToken. multi: truevergessen bei einem Provider → er überschreibt die ganze Liste, statt etwas hinzuzufügen.- Ein Interface als Token:
inject(MyInterface)funktioniert nicht — Interfaces existieren zur Laufzeit nicht. Nutze ein Token oder eine abstrakte Klasse. - Langsame Initializer verzögern den Start der Anwendung. Lade nur das, was vor dem ersten Rendern unbedingt nötig ist.
Spickzettel
new InjectionToken<T>('NAME') // jemand muss ihn bereitstellen
new InjectionToken<T>('NAME', { providedIn: 'root', factory }) // mit Standardwert
{ provide: TOKEN, useValue: value }
{ provide: TOKEN, useFactory: () => create(inject(Other)) }
{ provide: TOKEN, useExisting: OtherToken }
{ provide: TOKEN, useClass: Impl, multi: true } // wird in einem Array gesammelt
inject(TOKEN) // NG0201, wenn er fehlt
inject(TOKEN, { optional: true }) // null, wenn er fehlt
makeEnvironmentProviders([...]) // nur in app.config / Routen
provideAppInitializer(() => inject(Config).load()) // läuft vor dem ersten Rendern