La fonction inject() a révolutionné la gestion des dépendances en Angular. Introduite en version 14, elle offre une syntaxe plus flexible et plus lisible que le pattern historique du constructeur. Pourtant, les deux coexistent encore largement dans les codebases modernes, et le choix entre elles n'est pas trivial. Cet article explore les différences réelles, les cas d'usage de chacune, et les pièges à éviter pour prendre la bonne décision dans votre projet.
Le pattern historique : injection par constructeur
L'injection de dépendances via constructeur a été le fondement d'Angular depuis le départ. Cette approche repose sur les décorateurs TypeScript et les métadonnées de réflexion pour identifier les dépendances. Vous déclarez un constructeur typé, Angular analyse les types des paramètres, et injecte les services correspondants au moment de l'instantiation. Cette approche fonctionne bien et reste extrêmement robuste, avec un système de compilation AOT mature et une intégration profonde dans le framework.
Le constructeur impose cependant une structure stricte. Chaque dépendance doit être un paramètre nommé, et l'ordre compte. Les services doivent être injectables, enregistrés dans un provider, et le compilateur Angular doit résoudre les métadonnées. Dans les composants complexes avec de nombreuses dépendances, les constructeurs deviennent verbeux et difficiles à maintenir. De plus, il n'est pas possible de dépendre d'un service de manière lazy ou conditionnelle : on injecte tout au moment de la création du composant.
@Component({
selector: 'app-user',
template: `<p>{{ user$ | async }}</p>`
})
export class UserComponent implements OnInit {
constructor(
private userService: UserService,
private route: ActivatedRoute,
private changeDetectorRef: ChangeDetectorRef
) {}
ngOnInit() {
// Logique d'initialisation
}
}```La révolution : inject() et la composition fonctionnelle
inject() change de paradigme. Appelée directement dans le corps du composant ou du service, elle retourne l'instance demandée sans passer par le constructeur. Cette fonction fonctionne dans n'importe quel contexte d'injection actif : composants, services, intercepteurs, guards, même dans des fonctions utilitaires si elles sont appelées au bon moment. Elle rend le code plus lisible, plus flexible et plus proche de la composition fonctionnelle moderne en TypeScript.
Avec inject(), vous pouvez déclarer les dépendances au point d'utilisation, plutôt que de les énumérer en haut du fichier. Cela améliore la clarté du code : le lecteur voit immédiatement où tel service est utilisé. Vous pouvez aussi récupérer un service de manière conditionnelle ou lazy, en l'appelant seulement si vous en avez besoin. Enfin, elle joue bien avec les Signals et les composants standalone, qui sont l'avenir d'Angular.
@Component({
selector: 'app-user',
template: `<p>{{ user$ | async }}</p>`,
standalone: true
})
export class UserComponent implements OnInit {
private userService = inject(UserService);
private route = inject(ActivatedRoute);
private cdr = inject(ChangeDetectorRef);
ngOnInit() {
// Logique d'initialisation
}
}```Quand choisir inject() ?
Privilégiez inject() dans trois cas principaux. D'abord, pour les composants standalone et les services modernes : c'est la norme de facto en Angular 17+. Deuxièmement, quand vous avez beaucoup de dépendances et que le constructeur devient illisible. Troisièmement, pour les patterns avancés comme l'injection conditionnelle ou l'accès au contexte d'injection lui-même via injector.get().
Un exemple concret : vous construisez un component qui a besoin d'un service facultatif selon une flag de configuration. Avec le constructeur, vous devez utiliser @Optional() et gérer la valeur null. Avec inject(), vous pouvez vérifier une condition et appeler inject() seulement si nécessaire. Cela rend le code plus expressif et moins encombré.
Quand rester sur le constructeur ?
Le constructeur reste pertinent dans certains contextes. Si votre projet est une codebase d'entreprise stable avec des dizaines de composants hérités, migrer tout d'un coup introduce du risque et peu de bénéfice immédiat. Le constructeur est aussi plus testable dans les scénarios où vous injectez des mocks manuellement, car vous pouvez passer les dépendances directement au constructeur sans utiliser TestBed.
De plus, si vous travaillez avec des librairies tierces qui attendent des constructeurs typés (certains frameworks de validation ou ORM), le constructeur offre une meilleure compatibilité. Enfin, pour les composants très simples avec une ou deux dépendances, changer de pattern n'apporte rien. La vraie question n'est pas « quel est le meilleur », mais « quel est le meilleur pour ce contexte ».
Le piège courant : oublier le contexte d'injection
Le piège le plus fréquent est d'appeler inject() en dehors d'un contexte d'injection actif. Cette fonction ne fonctionne que durant la construction d'un composant ou d'un service, ou dans une fonction appelée directement depuis ce contexte. Si vous l'appelez dans un callback asynchrone, un event listener ou une fonction utilitaire appelée plus tard, Angular lèvera une erreur : « inject() must be called from an injection context ».
Cela arrive souvent quand on veut créer une fonction helper réutilisable qui a besoin d'un service. La bonne approche est de passer le service en paramètre, ou de créer un factory pattern avec un service qui enveloppe la logique. Ne tentez jamais inject() dans un setTimeout, une Promise .then(), ou un event listener attaché après l'initialisation.
Stratégie de migration progressive
Si votre projet est sur Angular 14+ et que vous voulez moderniser, migrez graduellement. Commencez par les nouveaux composants standalone en utilisant inject(). Pour les composants existants, migrez d'abord ceux qui sont les plus critiques ou les plus complexes, ceux où inject() apporte vraiment un bénéfice lisibilité. Testez bien à chaque étape, car le changement affecte la réflexion TypeScript et les métadonnées du compilateur.
Une bonne pratique est de créer une convention d'équipe : « tout nouveau composant standalone utilise inject(), les composants hérités restent au constructeur jusqu'à refactorisation ». Cela évite le chaos et laisse du temps pour apprendre et s'adapter. Les tests unitaires vous aideront à valider que l'injection fonctionne correctement après migration.
Conclusion : pragmatisme plutôt que dogmatisme
En 2025, inject() est la direction d'Angular. Elle simplifie le code, joue mieux avec les composants standalone et les Signals, et offre plus de flexibilité. Cependant, le constructeur n'est pas obsolète : il reste valide, performant et approprié dans certains contextes. La vraie sagesse est de comprendre les deux, de choisir consciemment selon votre cas d'usage, et de migrer progressivement plutôt que de forcer un changement dogmatique. Commencez par inject() sur les nouveaux codes, mesurez l'impact réel sur la maintenabilité et la performance, et ajustez votre stratégie en fonction de ce que votre équipe apprend.