~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 suffit : sur un projet v20 laissé dans sa configuration par défaut, le schematic fait le ménage partout, specs comprises. Attention aux applications générées à partir de la v21 : elles ne déclarent plus de tsConfig sur leur cible test (les bibliothèques, si), et le schematic laisse alors tes specs en injection par constructeur, un angle mort que détaille le tour des 15 migrations ng generate.
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: The X token injection failed. inject() function 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.