📧 Reste informé(e) !

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

S'inscrire gratuitement

~7 min de lecture

@Service : le décorateur qui remplace @Injectable (et quand il ne faut pas l'utiliser)

Depuis Angular 6, tu écris la même incantation en haut de chaque service :

@Injectable({ providedIn: 'root' })
export class PostsService { /* ... */ }

Tu l'as tapée des centaines de fois. providedIn: 'root', c'est devenu un réflexe, au point qu'on oublie que @Injectable peut faire bien d'autres choses, et que dans 90% des cas on n'utilise qu'une seule de ses configurations.

Angular 22 prend acte de ce constat et introduit un nouveau décorateur : @Service. L'idée est simple : encoder le cas le plus fréquent (un singleton applicatif) dans un décorateur dédié, plus court, et qui pousse vers les patterns modernes. Ce n'est pas un remplacement total de @Injectable : c'est un outil plus précis pour le cas dominant.

Dans cet article on va voir ce que @Service fait exactement, en quoi il diffère de @Injectable ligne par ligne, le garde-fou volontaire qu'il impose, comment scoper un service à un composant avec autoProvided, et surtout la table de décision : quand basculer, et quand garder @Injectable.


Le problème que @Service résout

@Injectable est un couteau suisse. Il accepte une configuration riche :

  • providedIn: 'root' | 'platform' | 'any' ou un module / une route,
  • l'injection par constructeur (héritée de l'époque AngularJS et des NgModule),
  • et il sert de support à des stratégies de providers avancées (useClass, useFactory...).

Le souci, c'est que cette flexibilité a un coût cognitif. Pour le service le plus banal qui soit (un singleton applicatif qui injecte deux-trois dépendances via inject()), tu paies quand même le prix de devoir te souvenir d'écrire { providedIn: 'root' }. Oublie-le, et ton service n'est pas tree-shakable, voire pas fourni du tout.

@Service part d'un principe : le bon défaut ne devrait rien demander. Un singleton root, c'est le cas par défaut, donc il devient implicite.


La forme de base : avant / après

Voici un service classique écrit avec @Injectable, puis sa traduction avec @Service.

Avant, avec @Injectable :

import { Injectable, inject } from '@angular/core';
import { HttpClient } from '@angular/common/http';

@Injectable({ providedIn: 'root' })
export class PostsService {
  private http = inject(HttpClient);

  getPosts() {
    return this.http.get<Post[]>('/api/posts');
  }
}

Après, avec @Service :

import { Service, inject } from '@angular/core';
import { HttpClient } from '@angular/common/http';

@Service()
export class PostsService {
  private http = inject(HttpClient);

  getPosts() {
    return this.http.get<Post[]>('/api/posts');
  }
}

Deux changements, zéro régression de comportement :

  1. @Injectable devient @Service,
  2. on supprime { providedIn: 'root' }, c'est le défaut.

Le service reste un singleton fourni dans le root injector, exactement comme avant. Tout composant qui fait inject(PostsService) reçoit la même instance. Rien d'autre ne change dans tes consommateurs.


Le garde-fou : inject() uniquement, pas de constructeur

C'est la différence sémantique la plus importante, et celle qu'il faut retenir.

@Injectable autorise les deux styles d'injection :

// Style historique : injection par constructeur
@Injectable({ providedIn: 'root' })
export class OrderService {
  constructor(private http: HttpClient) {}
}

// Style moderne : fonction inject()
@Injectable({ providedIn: 'root' })
export class OrderService {
  private http = inject(HttpClient);
}

@Service, lui, ne supporte que inject(). L'injection par constructeur n'est pas permise : c'est un choix délibéré de l'équipe Angular, un garde-fou qui pousse vers le pattern moderne.

// ❌ À éviter avec @Service : l'injection par constructeur n'est pas le pattern visé
@Service()
export class OrderService {
  constructor(private http: HttpClient) {}
}

// ✅ Le seul style avec @Service
@Service()
export class OrderService {
  private http = inject(HttpClient);
}

Pourquoi ce choix ?

L'injection par constructeur s'appuie sur les métadonnées de types TypeScript émises à la compilation (emitDecoratorMetadata). C'est un mécanisme qui a vieilli : il rend l'arbre de dépendances opaque pour le compilateur, complique l'héritage de classes, et oblige à traîner du reflect-metadata. inject(), à l'inverse, prend un token explicite, fonctionne sans métadonnées, et compose mieux (tu peux l'appeler dans une fonction factory, un computed, un champ initialisé).

En faisant de @Service une porte qui ne s'ouvre que sur inject(), Angular n'invente pas une nouvelle règle : il matérialise dans le typage une recommandation qui existait déjà. Tu ne peux plus régresser par accident.

⚠️ Attention au piège de la doc : la page d'erreur NG0204 montre un exemple @Service() avec un constructeur. C'est une illustration générique du « décorateur manquant » et non une validation de l'injection par constructeur avec @Service. Le guide officiel sur les services est explicite : « Constructor-based dependency injection is not supported. The @Service decorator only supports the inject() function. » Le décorateur vient d'ailleurs d'une proposition expérimentale qui posait déjà inject() comme seul mode d'injection.


Scoper un service à un composant : autoProvided: false

Le défaut de @Service, c'est l'auto-provisioning dans le root injector. Mais tous les services ne doivent pas être des singletons globaux : un brouillon de formulaire, l'état d'un wizard, un store local à un panneau veulent une instance par composant.

Avec @Injectable, tu obtenais ça en omettant providedIn et en listant la classe dans les providers du composant. Avec @Service, tu désactives l'auto-provisioning avec autoProvided: false :

import { Service, signal } from '@angular/core';

@Service({ autoProvided: false })
export class DraftPostService {
  title = signal('');
  body = signal('');
}

Le service n'est alors fourni nulle part automatiquement. C'est à toi de le déclarer là où tu veux un cycle de vie scopé :

import { Component, inject } from '@angular/core';
import { DraftPostService } from './draft-post-service';

@Component({
  selector: 'app-draft-panel',
  providers: [DraftPostService], // une instance neuve par <app-draft-panel>
  templateUrl: './draft-panel.html',
})
export class DraftPanel {
  protected draft = inject(DraftPostService);
}

Chaque instance de DraftPanel reçoit son propre DraftPostService, détruit avec le composant. C'est l'équivalent moderne du @Injectable() « nu » + providers, mais le nom de l'option (autoProvided: false) rend l'intention explicite : je sors volontairement du cas singleton root.

L'option factory

@Service accepte aussi une fonction factory pour contrôler la création de l'instance singleton :

@Service({ factory: () => new PaymentService(/* ... */) })
export class PaymentService { /* ... */ }

Utile quand l'instanciation demande une logique particulière. Dans la grande majorité des cas, tu n'en auras pas besoin : le constructeur par défaut suffit.


La table de décision : @Service ou @Injectable ?

Voici la règle pratique.

Tu veux... Décorateur
Un singleton applicatif (root) avec inject() @Service()
Un service scopé à un composant/route avec inject() @Service({ autoProvided: false }) + providers
De l'injection par constructeur @Injectable
Un provider avancé : useClass, useValue, useExisting, useFactory @Injectable
Un scope 'platform' ou 'any' @Injectable
Migrer du legacy sans tout réécrire @Injectable (puis migration progressive)

La synthèse tient en une phrase : @Service couvre le cas par défaut, un nouveau singleton qui injecte ses dépendances avec inject(). Dès que tu sors de ce cas (constructeur, providers avancés, scopes exotiques), @Injectable reste l'outil.

@Injectable n'est ni déprécié ni en sursis. Les deux décorateurs cohabitent : @Service est l'expression resserrée du cas dominant, @Injectable reste la boîte à outils complète.


Faut-il migrer tout ton code ?

Non, et surtout pas dans la précipitation.

@Service brille sur le code neuf : un nouveau service, tu pars directement dessus. Pour l'existant, le gain est purement cosmétique tant que tes services utilisent déjà inject() : ils fonctionnent parfaitement avec @Injectable.

Une stratégie raisonnable :

  1. Nouveau service@Service() par défaut.
  2. Service touché lors d'une feature → si déjà en inject() + providedIn: 'root', bascule-le en @Service() au passage (deux lignes).
  3. Service en injection par constructeur → migre d'abord vers inject(), puis éventuellement vers @Service. L'inverse est impossible (rappel : @Service refuse le constructeur).
  4. Tout le reste → laisse-le tranquille. Une PR « migration de masse @Injectable vers @Service » apporte du bruit de diff sans valeur fonctionnelle.

Le piège classique : NG0204

Si tu vois cette erreur après avoir touché un service :

NG0204: Can't resolve all parameters for X: (?, ?, ?)

Elle signifie qu'Angular n'arrive pas à résoudre les dépendances d'une classe. Les causes fréquentes :

  • la classe n'a ni @Service ni @Injectable,
  • elle utilise l'injection par constructeur avec un paramètre dont le type n'a pas de provider,
  • un InjectionToken est consommé sans provider configuré.

Avec @Service, comme l'injection se fait via inject() (token explicite), le scénario « paramètre de constructeur non résolvable » disparaît de fait : c'est un bénéfice indirect du garde-fou. Le piège résiduel : oublier le décorateur tout court sur une classe injectée. Pas de décorateur = pas de métadonnées DI = NG0204.


En résumé

  • @Service() = @Injectable({ providedIn: 'root' }) en plus court : singleton root, auto-fourni, sans config.
  • Il n'accepte que inject() : l'injection par constructeur est volontairement interdite. C'est une bonne nouvelle, le pattern moderne devient le seul chemin.
  • autoProvided: false désactive l'auto-provisioning pour scoper le service à un composant/route via providers.
  • @Injectable reste indispensable pour l'injection par constructeur, les providers avancés (useClass, useValue, useExisting, useFactory) et les scopes 'platform' / 'any'.
  • Migre le code neuf et les services que tu touches déjà ; n'orchestre pas de migration de masse.

@Service n'ajoute pas de capacité : il retire de la cérémonie sur le cas que tu écris le plus souvent, et il transforme une bonne pratique (inject()) en seule option possible. C'est exactement la trajectoire d'Angular depuis la v17 : moins de magie, des défauts qui tombent juste, et des garde-fous qui rendent le mauvais code plus difficile à écrire que le bon.


Pour le panorama complet de la release, voir Angular 22 : le guide complet de la release signal-first.

📧 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.