Angular 14 a introduit la fonction inject() comme alternative au constructeur classique pour l'injection de dépendances. Depuis, les équipes oscillent entre nostalgie du constructeur et adoption du nouveau paradigme. La vérité est plus nuancée : inject() n'est pas universellement supérieur, et son adoption stratégique dépend du contexte du composant ou du service. Comprendre les différences de timing, de lisibilité et de compatibilité avec les nouvelles primitives réactives devient essentiel pour écrire du code maintenable.
Le timing d'exécution : une différence invisible mais critique
Le constructeur s'exécute avant que le composant entre dans sa phase d'initialisation Angular. À ce moment, les directives parentes n'ont pas encore été appliquées, et les valeurs injectées via tokens de configuration personnalisés ne sont pas garanties d'être disponibles. inject(), en revanche, s'exécute dans le contexte d'injection courant—souvent dans la méthode de création du composant ou du service. Cela signifie que les providers définis dans le composant lui-même, ou hérités de parents, sont visibles. Concrètement, si vous injectez un service via un token fourni par un composant parent, le constructeur échouera silencieusement ou lèvera une erreur, alors que inject() trouvera le bon provider. Cette différence devient criante dans les patterns avancés comme les tokens injectés dynamiquement ou les configurations au niveau du composant.
const MyService = inject(ConfigService); // Voit les providers du composant parent
@Component({ providers: [{ provide: ConfigService, useValue: customConfig }] }) export class MyComponent { constructor(private config: ConfigService) {} // Peut ne pas voir le provider personnalisé }
Signaux et réactivité : inject() gagne naturellement
Avec l'arrivée de effect(), computed() et des signaux, inject() s'intègre plus fluidement. Un signal injecté via inject() dans un effect() ou computed() crée automatiquement une dépendance réactive—Angular suit les appels à .value() et met à jour le calcul. Avec le constructeur, vous devez gérer manuellement les souscriptions RxJS ou les ChangeDetectionStrategy.OnPush. Prenez un exemple simple : vous injectez un signal de thème et voulez mettre à jour une classe CSS. Avec inject() et effect(), c'est une ligne. Avec le constructeur et RxJS, vous devez créer un Subscription, la détruire en OnDestroy, et gérer la détection de changements. Ce n'est pas insurmontable, mais c'est plus verbeux. Les nouvelles équipes qui commencent avec les signaux trouveront inject() plus cohérent avec leur modèle mental.
export class ThemeComponent { private themeSignal = inject(ThemeService).currentTheme;
constructor(private renderer: Renderer2) { effect(() => { const theme = this.themeSignal(); this.renderer.addClass(document.body, theme-${theme} ); }); } }
Lisibilité et introspection : le débat persiste
Le constructeur reste plus facile à lire pour les développeurs Angular classiques. Les dépendances sont visibles en un coup d'œil : les paramètres typés du constructeur servent de contrat lisible. inject() disperse les déclarations dans le corps du composant ou du service, ce qui peut sembler moins structuré au premier abord. Cependant, inject() offre une flexibilité supérieure : vous pouvez injecter conditionnellement, avec des valeurs par défaut, ou uniquement dans les branches utilisées. Avec le constructeur, vous injectez tout, même ce qui n'est pas nécessaire. Pour les services avec 5-7 dépendances, le constructeur devient un monstre. Pour les composants simples avec 2-3 dépendances, le constructeur reste plus lisible. L'introspection automatisée (outils de refactoring, linters) fonctionne mieux avec le constructeur, car la syntaxe est standardisée. inject() exige des heuristiques plus sophistiquées.
Testabilité : inject() complique légèrement
En test, injecter des mocks via le constructeur est trivial : vous passez les dépendances au new. Avec inject(), vous devez configurer le TestBed et fournir les mocks via providers. Cela n'est pas un obstacle—c'est le pattern standard depuis Angular 2—mais cela signifie que les tests de services avec inject() demandent toujours un TestBed, alors que les tests de services avec constructeur peuvent utiliser des instances simples. Pour les composants, la différence s'estompe : les deux approches exigent un TestBed. Mais pour les fonctions utilitaires ou les services purs que vous testez en isolation, le constructeur reste plus léger. Si vous écrivez beaucoup de tests unitaires rapides, cela compte.
Le piège courant : mélanger les deux approches
La plus grosse erreur que les équipes commettent est de mélanger inject() et le constructeur au hasard. Un service utilise inject(), un autre le constructeur, et soudain la cohérence se brise. Les nouveaux venus se demandent quand utiliser quoi. Pire, si vous hériterez d'une classe (rare mais possible), les dépendances du parent injectées via inject() ne sont pas accessibles à la classe enfant—vous devez les redéclarer ou les passer via le constructeur. Cette confusion génère des bugs subtils et rend le code difficile à maintenir. La règle doit être claire au niveau de l'équipe : ou vous adoptez inject() partout (moderne, cohérent avec les signaux), ou vous restez au constructeur (compatible, lisible). Un tiers chemin—utiliser inject() pour les signaux et le constructeur pour les services RxJS—crée plus de friction qu'il n'en résout.
Quand choisir inject() : la matrice pratique
Utilisez inject() si votre projet est nouveau ou en refonte, que vous ciblez Angular 17+, et que vous adoptez les signaux. Utilisez-le aussi si vous avez besoin d'injection conditionnelle, de dépendances optionnelles (avec inject(Service, { optional: true })), ou si vous injectez des tokens personnalisés fournis par des composants parents. Pour les services d'infrastructure simples (HTTP, Logger, Config), inject() apporte peu de bénéfice et le constructeur reste plus lisible. Si votre équipe inclut des développeurs moins familiers avec les patterns modernes, le constructeur reste un bon point de départ. Pour les libs que vous maintenez et qui doivent supporter Angular 12-18, restez au constructeur : inject() nécessite Angular 14+, et les utilisateurs de versions antérieures seront bloqués.
Conclusion : pragmatisme avant dogme
Il n'y a pas de réponse universelle. inject() est plus moderne, plus flexible, et s'intègre mieux aux signaux. Mais le constructeur reste valide, plus lisible pour certains contextes, et plus facile à tester en isolation. La bonne approche est de prendre une décision au niveau de l'équipe et de la documenter. Créez une rule ESLint simple (@angular-eslint) pour l'enforcer, et évitez les débats ad-hoc à chaque pull request. Si vous passez à inject(), commencez par les services métier et les composants qui manipulent des signaux. Les services d'infrastructure et les composants de présentation simples peuvent attendre. Surtout, ne laissez pas la modernité pour la modernité vous dicter un refactoring inutile d'une base de code qui fonctionne déjà bien. Angular a cette force : il vous permet de choisir, et c'est votre responsabilité de le faire sagement.