📧 Reste informé(e) !

Reçois les derniers articles et conseils EasyAngularKit directement dans ta boîte mail.

S'inscrire gratuitement

~7 min de lecture

inject() : 4 flags qu'on oublie (et qui ferment tes bugs DI hiérarchique)

Tu débogues une NullInjectorError. Tu ajoutes un provider. Maintenant c'est ton composant enfant qui reçoit la mauvaise instance. Tu ajoutes un provider local. Maintenant ton test crashe parce qu'aucune cascade ne descend jusqu'au mock. Tu finis par dupliquer la config dans trois providers: [] différents. Tu te dis que la DI Angular est compliquée.

Elle ne l'est pas. Tu utilises juste inject(Truc) sans jamais regarder son deuxième argument. Cet argument prend quatre flags (optional, self, skipSelf, host) qui changent radicalement la résolution. Sur les projets que j'audite, je vois optional une fois sur trente, self jamais, skipSelf jamais, host jamais. Et pourtant ces flags ferment des bugs qu'on contourne aujourd'hui avec des hacks (re-providers, fallback ??, services globaux qui ne devraient pas l'être).

Cet article fait le tour des quatre flags sur des cas concrets Angular 17+, avec le before/after du code que tu écris probablement aujourd'hui.

Le modèle hiérarchique en 30 secondes

Quand tu fais inject(Service), Angular résout la dépendance en partant de l'élément courant et en remontant la chaîne :

  1. Le node injector du composant courant (ses propres providers: [])
  2. Le node injector de son parent (composant ancestor)
  3. ...jusqu'à la racine
  4. Puis le environment injector (root, lazy-loaded route, provideXxx() global)

Le défaut, c'est "parcours toute la chaîne, prends la première résolution qui matche, sinon throw". Les quatre flags modifient ce parcours :

  • optional : "ne throw pas si rien n'est trouvé, retourne null"
  • self : "regarde UNIQUEMENT mon node injector, ne remonte pas"
  • skipSelf : "saute mon node injector, commence à mon parent"
  • host : "remonte mais arrête-toi à l'élément hôte (utile pour ng-content)"

Ces flags se combinent ({ skipSelf: true, optional: true } est très utile). On y vient.

Flag 1 : { optional: true } pour les dépendances vraiment optionnelles

Le cas concret : tu as un service de tracking analytics fourni explicitement dans un providers: [] (pas en providedIn: 'root', sinon il serait toujours résoluble). Il est provisionné en prod, pas en dev. Sans flag, ton code crashe en dev. Avec flag, tu reçois null et tu skip.

Avant

import { ChangeDetectionStrategy, Component, inject } from '@angular/core';
import { Analytics } from './analytics';

@Component({
  selector: 'app-checkout',
  changeDetection: ChangeDetectionStrategy.OnPush,
  template: `<button (click)="pay()">Payer</button>`,
})
export class Checkout {
  private readonly analytics = inject(Analytics);

  pay(): void {
    this.analytics.track('checkout_clicked');
    // En dev sans provider Analytics : NullInjectorError à la construction du composant.
  }
}

Après

import { ChangeDetectionStrategy, Component, inject } from '@angular/core';
import { Analytics } from './analytics';

@Component({
  selector: 'app-checkout',
  changeDetection: ChangeDetectionStrategy.OnPush,
  template: `<button (click)="pay()">Payer</button>`,
})
export class Checkout {
  private readonly analytics = inject(Analytics, { optional: true });

  pay(): void {
    this.analytics?.track('checkout_clicked');
  }
}

Note : inject(X, { optional: true }) typé X | null. TypeScript te force le ?. ou le if, ce qui est l'effet recherché. Pas de cast, pas de !. Si tu te retrouves à faire inject(X, { optional: true })! partout, c'est que la dépendance n'est pas optionnelle, c'est juste que ta config de DI est bancale.

Flag 2 : { self: true } pour bloquer la remontée

Le cas concret : tu écris une directive Field qui doit récupérer le NgControl posé sur SON élément hôte (typiquement un [formControlName]), pas celui d'un NgForm parent. Sans flag, Angular remonte et te file le mauvais.

{ self: true } dit "regarde uniquement les providers de MON node injector. Si rien n'est trouvé, throw (ou retourne null si combiné avec optional)".

import { Directive, inject } from '@angular/core';
import { NgControl } from '@angular/forms';

@Directive({
  selector: 'input[appField]',
})
export class Field {
  private readonly control = inject(NgControl, { self: true, optional: true });
  // Sans { self: true }, Angular remonterait jusqu'au NgForm parent
  // et tu surveillerais le mauvais control.

  constructor() {
    this.control?.valueChanges?.subscribe((next) => {
      console.log('value of THIS input:', next);
    });
  }
}

C'est exactement le pattern de MatInput dans Angular Material : la directive est posée sur un <input formControlName="email">, et inject(NgControl, { self: true, optional: true }) récupère le FormControlName directive accroché au même élément. Sans le flag, tu attraperais FormGroupDirective du parent.

Combiné avec optional, { self: true, optional: true } veut dire "si l'élément hôte porte cette dépendance, donne-la-moi, sinon je m'en passe". C'est la signature canonique des directives qui s'enrichissent d'un control sans le rendre obligatoire.

Flag 3 : { skipSelf: true } pour aller chercher plus haut

Le miroir de self. "Saute mon node injector, commence à mon parent et remonte normalement."

Le cas concret : un composant NestedConfig qui fournit une version locale d'un ThemeService à ses enfants, mais qui veut LUI accéder au ThemeService parent pour le hériter.

import { ChangeDetectionStrategy, Component, inject } from '@angular/core';
import { Theme } from './theme';

@Component({
  selector: 'app-nested-config',
  changeDetection: ChangeDetectionStrategy.OnPush,
  providers: [Theme], // une nouvelle instance pour mes enfants
  template: `<ng-content />`,
})
export class NestedConfig {
  private readonly parentTheme = inject(Theme, { skipSelf: true });
  private readonly localTheme = inject(Theme); // l'instance que je viens de provider

  constructor() {
    this.localTheme.inheritFrom(this.parentTheme);
  }
}

Sans { skipSelf: true }, inject(Theme) te retournerait l'instance que TU viens de fournir. Tu hériterais de toi-même, ce qui ne fait rien. Avec le flag, tu attrapes l'instance du contexte parent et tu en clones les valeurs dans la tienne.

Variante avec optional : { skipSelf: true, optional: true }. Utile pour les cas "si un parent fournit la config, hérite, sinon démarre vide". Pattern qu'on retrouve dans @angular/cdk/overlay, dans les composants tree, et dans toutes les libs de form composable.

Flag 4 : { host: true } pour les directives projetées

Le moins connu, mais le plus crucial quand tu écris des composants/directives projetés via <ng-content>.

Le cas : ton composant FormField (le wrapper avec label et erreur) accepte n'importe quel input via ng-content. Tu veux que ce input puisse inject() une FormField instance pour s'enregistrer auprès d'elle, mais SEULEMENT si elle est l'hôte direct, pas un FormField plus haut.

import { ChangeDetectionStrategy, Directive, inject } from '@angular/core';
import { FormField } from './form-field';

@Directive({
  selector: 'input[appFormFieldControl]',
})
export class FormFieldControl {
  private readonly field = inject(FormField, { host: true, optional: true });

  constructor() {
    this.field?.registerControl(this);
  }
}

Avec { host: true }, Angular remonte la chaîne jusqu'au host element du component qui projette le contenu, puis s'arrête. Sans ce flag, si un <app-form-field> est imbriqué dans un autre <app-form-field>, ta directive risque de s'enregistrer auprès du mauvais. Avec le flag, tu garantis "le FormField qui m'accueille via ng-content, pas un ancêtre."

Ce besoin de borner la résolution à l'hôte projeté revient dans toutes les libs de form composables (le couple MatFormField / MatFormFieldControl d'Angular Material en est l'exemple canonique, même si Material relie surtout le wrapper à son contrôle via une content query @ContentChild(MatFormFieldControl)). Si tu as déjà écrit une lib UI de form, tu as croisé ce besoin sans toujours connaître le flag.

Attention au piège classique : { host: true } n'a de sens QUE pour une directive ou un composant projeté via ng-content. Si tu l'utilises sur un service en providedIn: 'root' ou dans un component qui n'est pas projeté, le flag ne change rien d'utile (Angular trouve déjà la dépendance et s'arrête). Le vrai cas d'usage : tu écris une directive qui s'attache à un input dans un slot, et tu veux interdire la remontée au-delà du wrapper qui projette.

Le duo gagnant : { skipSelf, optional } et { self, optional }

Les deux combinaisons que tu écris le plus souvent en vrai :

  • { self: true, optional: true } : "je m'enregistre si ma propre classe l'a fourni, sinon je n'en veux pas". Typique d'un composant qui se déclare comme un NgControl mais ne veut pas piquer le NgControl du formulaire parent.
  • { skipSelf: true, optional: true } : "je veux la version parente si elle existe, sinon je m'en passe". Typique pour les designs "override-able" type theming ou config en cascade.
import { ChangeDetectionStrategy, Component, inject, signal } from '@angular/core';
import { LocaleConfig } from './locale-config';

@Component({
  selector: 'app-date-picker',
  changeDetection: ChangeDetectionStrategy.OnPush,
  providers: [{ provide: LocaleConfig, useFactory: createPickerLocale }],
  template: `{{ formatted() }}`,
})
export class DatePicker {
  private readonly parentLocale = inject(LocaleConfig, {
    skipSelf: true,
    optional: true,
  });
  private readonly localLocale = inject(LocaleConfig);

  protected readonly formatted = signal('');

  constructor() {
    if (this.parentLocale) {
      this.localLocale.mergeFrom(this.parentLocale);
    }
  }
}

Récap actionnable

Quand tu écris inject(Truc), demande-toi à voix haute :

  1. Cette dépendance peut-elle manquer légitimement ? Si oui, { optional: true }. Et acte le ?. dans ton code, ne triche pas avec un !.
  2. Est-ce que je viens de la fournir dans providers: [] ? Si oui ET que tu veux UNIQUEMENT ta propre instance, { self: true }. Si tu veux la parente pour hériter, { skipSelf: true }.
  3. Suis-je une directive projetée via ng-content qui doit parler à son hôte direct ? { host: true }. Sinon, oublie ce flag.

Si tu vois dans ta codebase un providers: [] dupliqué à plusieurs niveaux pour "forcer" la bonne instance, c'est qu'un { skipSelf: true } ou un { self: true } te manquait. Si tu vois un service global injecté partout uniquement parce que vous n'arriviez pas à le scoper, c'est { optional: true } + provider au bon niveau qui fait le job.

Les quatre flags existent depuis Angular 14 (et leur version decorator depuis Angular 2). Ce n'est pas nouveau. Ce qui change avec inject(), c'est que la signature est devenue lisible : un objet d'options au lieu de quatre décorateurs empilés. Plus aucune raison de les ignorer.

Au passage : oublie @Optional() @Self() @SkipSelf() @Host()

Si tu vois encore du code de cette forme :

constructor(
  @Optional() @SkipSelf() private parentTheme: Theme | null,
) {}

Tu peux migrer en une ligne :

private readonly parentTheme = inject(Theme, { skipSelf: true, optional: true });

Même comportement, type inférence correcte (Theme | null automatiquement), plus de constructor à maintenir, et compatible avec les fields initializer. Les décorateurs ne sont pas dépréciés, mais ils n'apportent plus rien à un code en API moderne (signals, inject(), components sans constructor). Mixer les deux styles dans une même classe fonctionne — l'ordre de résolution reste déterministe — mais ça nuit à la lisibilité, autant tout basculer en inject(). Le vrai piège DI est ailleurs : appeler inject() hors d'un contexte d'injection (après un await, dans un callback), ce qui jette NG0203.

La règle simple : depuis Angular 16, inject() partout, flags via l'objet d'options. Tes 4 décorateurs DI disparaissent du diff. Tes bugs de DI aussi, parce que pour la première fois tu peux les LIRE en une ligne.

📧 Reste informé(e) !

Reçois les derniers articles et conseils EasyAngularKit directement dans ta boîte mail.

S'inscrire gratuitement

AngularKit

Suite d'outils pour développeurs Angular francophones. Apprends, modernise tes réflexes, audite ta codebase.

Produits

Contact

Légal

© 2026 AngularKit. Tous droits réservés.