~5 min de lecture
Angular v20 : style guide et migration depuis v19, le guide complet
Angular v20 ne s'est pas contentée d'évolutions techniques. Elle a aussi réécrit son style guide — plus court, plus pragmatique, opinionnated. Et la migration depuis la v19 réserve quelques pièges, surtout côté tests.
J'ai migré mon stack Angular/Nx/Vitest vers la v20, lu le nouveau style guide en entier, et appliqué les recommandations sur EasyAngularKit. Voici le retour brut : ce qui change vraiment, et ce qu'on aurait aimé savoir avant de lancer ng update.
TL;DR
- Style guide : organisation par feature (fini
components/,services/,directives/),inject()au lieu du constructeur,protectedau lieu depublic,readonlypar défaut, plus de suffixe.component.ts. La règle d'or : cohérence avant règles. - Migration : prévoir un upgrade manuel de
vite+vitest, configurer le zoneless danstest-setup.ts, lancerng generate @angular/core:injectpour faire le ménage dans l'injection par constructeur, remplacerBrowserDynamicTestingModuleparBrowserTestingModule. - Verdict : la v20 simplifie l'expérience dev sur la durée, mais la migration n'est pas un simple
ng update. Compter 1-2 jours sur une codebase moyenne.
Partie 1 — Le nouveau style guide
Un guide plus court. Et plus clair.
Premier choc : il est incroyablement plus court que les versions précédentes. La toute première règle donne le ton :
"When in doubt, prefer consistency (within a file)."
Autrement dit : la cohérence > les règles. C'est pragmatique, moderne, et pertinent — exactement l'esprit qu'on défend sur EasyAngularKit.
Fini les répertoires components/, services/, directives/
Le guide recommande d'organiser les fichiers par feature, et non par type technique. Plutôt que :
src/app/
components/
user-card.ts
user-list.ts
services/
user.service.ts
models/
user.model.ts
Le pattern préféré :
src/app/
user/
user-card.ts
user-list.ts
user.service.ts
user.model.ts
Ce découpage rend l'architecture lisible du point de vue métier, pas seulement technique. C'est aussi exactement le pattern de co-location que recommande la skill architecture-review qu'on utilise en interne sur EAK.
1 concept = 1 fichier (en général)
Il est désormais recommandé d'avoir un seul concept par fichier : un composant, une directive, un pipe.
Mais (et c'est là que c'est malin) : si plusieurs éléments sont profondément liés — par exemple une directive interne à un composant, ou un pipe utilisé uniquement par lui — les regrouper dans le même fichier reste acceptable.
En cas de doute : on découpe.
Vive inject() au lieu du constructeur
L'usage de inject() est désormais recommandé à la place de l'injection via constructeur.
// ❌ Avant
@Injectable()
class UserService {
constructor(private readonly http: HttpClient) {}
}
// ✅ Maintenant
@Injectable()
class UserService {
private readonly http = inject(HttpClient);
}
Et pour faciliter la transition, Angular fournit le schematic :
ng generate @angular/core:inject
Un seul appel sur toute la codebase, ça fait le ménage en une fois.
protected > public
Angular recommande désormais de privilégier protected aux propriétés public. La règle implicite : ce qui est dans la classe d'un composant n'est consommé que par son template — donc protected suffit, et c'est plus honnête sur l'intention.
@Component({...})
export class UserCard {
protected readonly user = input.required<User>();
protected readonly isExpanded = signal(false);
}
readonly par défaut
Toutes les propriétés initialisées par Angular (input(), output(), viewChild(), etc.) doivent être marquées readonly.
À titre personnel, je pousse plus loin : readonly partout, sauf si on a une bonne raison de faire autrement. Le compilateur attrape les mutations qu'on aurait laissé passer dans une review.
Pas de logique dans les templates
Petit rappel salutaire :
Le template n'est pas le bon endroit pour la logique.
Pas de {{ veryComplexExpression() }} ou de @if (deepCheck(obj?.nested?.value?.something?.truthy)). Tout passe par une computed() (ou un signal dérivé) côté TypeScript.
Fini les suffixes .component.ts, .directive.ts
Oui, c'est terminé. Plus besoin de user-card.component.ts — user-card.ts suffit, à condition que le nom soit explicite.
Un fichier doit parler de lui-même. On devrait pouvoir deviner son contenu sans avoir besoin de l'ouvrir.
Quand j'ai lancé un sondage sur le sujet, j'ai été surpris de voir que la majorité y est opposée. Mais je continue de penser qu'il faut un peu d'effort initial pour gagner en lisibilité globale.
Partie 2 — Retex migration v19 → v20
Maintenant que le décor est posé, place aux pièges concrets rencontrés pendant la migration.
Des tests qui ne s'exécutent plus
Surprise après ng update : des tests bloqués à l'état queued, sans exécution réelle. Le problème venait d'un décalage entre les versions de vite et vitest poussées par Angular et celles attendues par Nx.
Solution : upgrade manuel dans package.json :
{
"devDependencies": {
"vite": "7.0.5",
"vitest": "3.2.4"
}
}
Puis pnpm install + relancer la suite. Pas glorieux, mais ça débloque.
Le passage au zoneless dans les tests
La v20 pousse l'approche zoneless par défaut. Encore faut-il le configurer aussi côté tests — sinon les whenStable() et detectChanges() ne se comportent pas comme attendu.
Setup dans test-setup.ts :
@NgModule({
providers: [
provideZonelessChangeDetection(),
{ provide: ComponentFixtureAutoDetect, useValue: true },
],
})
export class ZonelessTestModule {}
getTestBed().initTestEnvironment(
[BrowserTestingModule, ZonelessTestModule],
platformBrowserTesting(),
);
⚠️ Attention :
provideExperimentalZonelessChangeDetection()a été renommé enprovideZonelessChangeDetection()sans mise à jour automatique par le schematic. Si tu vois cette API dans tes tests, c'est à remplacer à la main.
Erreurs d'injection par constructeur
Angular v20 est plus stricte sur les formes d'injection implicites. Résultat : des erreurs parfois obscures au runtime, du style NG0203: inject() must be called from an injection context.
// ❌ Forme qui pose problème en v20 stricte
@Injectable()
class ExampleService {
constructor(private readonly _httpClient: HttpClient) {}
}
// ✅ Forme à utiliser
@Injectable()
class ExampleService {
private readonly _httpClient = inject(HttpClient);
}
Solution officielle : le schematic de migration :
ng generate @angular/core:inject
Doc officielle de la migration inject.
Modules de test obsolètes
Quelques dépréciations à corriger pour garder un projet propre :
| Avant | Maintenant |
|---|---|
BrowserDynamicTestingModule |
BrowserTestingModule |
platformBrowserDynamicTesting() |
platformBrowserTesting() |
Pas de grosse conséquence, mais Angular signale ça en warning à chaque run de test. Autant nettoyer.
Récap : ce qu'il faut retenir
| Problème | Solution |
|---|---|
| Style : où mettre mes fichiers ? | Par feature, plus par type |
| Style : injection | inject() partout (ng generate @angular/core:inject) |
| Style : visibilité | protected plutôt que public |
| Style : suffixes | Noms explicites, plus de .component.ts |
| Tests bloqués | Upgrade manuel de vite / vitest |
provideExperimentalZonelessChangeDetection |
Remplacé par provideZonelessChangeDetection() |
| Erreurs d'injection au runtime | ng generate @angular/core:inject |
| Warnings sur modules de test | Migrer vers BrowserTestingModule |
Verdict
Malgré les frictions, Angular v20 simplifie vraiment l'expérience dev :
- Meilleur support de
Signals - Performances accrues avec le zoneless par défaut
- API plus stricte mais plus claire
- Style guide enfin pragmatique
Si tu prends le temps d'adapter ta stack, tu y gagnes sur la durée. Compter une journée pour la migration technique + une demi-journée pour appliquer le style guide (organisation par feature, inject(), readonly).
Tu veux apprendre Angular avec ces conventions dès le départ ? 👉 EasyAngularKit couvre tout le cycle, de la structure de projet aux tests zoneless.