Nouveautés, d'Angular 17 à 22
Ce que chaque version a ajouté depuis les composants standalone.
Vérifié en septembre 2026 · Angular 22.1 · environ 13 min
Entre novembre 2023 et juillet 2026, Angular a changé de langage de template, de modèle de réactivité et de défauts : une application neuve n'a plus ni NgModule, ni Zone.js, ni stratégie de détection de changements écrite. Ce cours est une carte datée de ce trajet, de la 17 à la 22.1 : pour chaque version, ce qu'elle a ajouté, le statut de chaque ajout et la façon dont ce statut a évolué, lu dans le CHANGELOG d'angular/angular, la feuille de route d'angular.dev et les déclarations de types de la 22.1.7 qu'utilise ce site. Chaque notion a son cours, auquel celui-ci renvoie : Bases, Signals, Formulaires, HttpClient, RxJS, État applicatif, Tests, et Performance pour OnPush, zoneless, @defer et le rendu serveur.
Lire une version d'Angular
Jusqu'à la 22, Angular publiait une version majeure tous les six mois, suivie d'une à trois mineures. La page « Versioning and releases » d'angular.dev annonce depuis la 22 un nouveau rythme : une majeure tous les douze mois et quatre à six mineures, d'où une 22.2 publiée le 23 septembre 2026 et une 23 prévue vers juin 2027. Une majeure peut casser le code, avec des migrations que ng update applique ; une mineure ne le doit pas, mais elle ajoute des API.
Une API nouvelle naît rarement stable. La même page définit trois statuts : expérimental, une API qui peut ne jamais devenir stable ou changer profondément ; developer preview, une API complète et soignée qui peut encore changer, même dans un correctif ; stable, protégée par la politique de dépréciation, qui garantit qu'une API dépréciée survit au moins une majeure. Les deux premiers statuts échappent au versionnage sémantique.
Le risque est concret. resource, expérimental de la 19.0 à la 21, a changé de forme en passant à la 20, sans période de dépréciation : l'option request et le paramètre du même nom que recevait le loader sont devenus params, et le statut, un enum ResourceStatus dans les déclarations de la 19.2.17, est devenu une union de chaînes, 'idle', 'loading' et les autres. Un code écrit pour la 19.2 ne compile plus en 20 ; c'est exactement ce que le statut annonçait. Le statut réel d'une API se lit dans le fichier de déclarations du paquet installé, là où l'éditeur le montre aussi.
// node_modules/@angular/core/types/core.d.ts (22.1.7), extraits.
// Le numero qui suit @publicApi est la version ou l'API est devenue stable.
/**
* Creates a writable signal whose value is initialized and reset by the linked, reactive computation.
*
* @publicApi 20.0
*/
declare function linkedSignal<D>(computation: () => D, options?: {...}): WritableSignal<D>;
/**
* @publicApi 22.0
*/
declare function resource<T, R>(options: ResourceOptions<T, R> & {...}): ResourceRef<T>;
/**
* Creates a resource representing a debounced version of the source signal.
* @experimental 22.0
*/
declare function debounced<T>(source: () => T, wait: NoInfer<DebounceTimer<T>>, ...): Resource<T>;
// node_modules/@angular/common/types/_common_module-chunk.d.ts (22.1.7)
/**
* @publicApi
*
* @deprecated 20.0
* Use the `@if` block instead. Intent to remove in a future major release
*/
declare class NgIf<T = unknown> {...}Ces marqueurs sont plus sûrs que la documentation, qui retarde parfois : le cours Formulaires relève une page d'angular.dev qui dit Signal Forms expérimental alors que le paquet le déclare stable. Le tableau situe les versions ; les dates sont celles des titres du CHANGELOG.
| Version | Publiée | Ce qui change la façon d'écrire |
|---|---|---|
| 17.0 | 8 novembre 2023 | @if, @for, @switch, @defer ; builder esbuild et Vite |
| 17.1 à 17.3 | janvier à mars 2024 | input(), model(), requêtes en signals, output() |
| 18.0 | 22 mai 2024 | zoneless expérimental ; @let en 18.1 |
| 19.0 | 19 novembre 2024 | standalone par défaut ; linkedSignal, resource |
| 20.0 | 28 mai 2025 | effect, linkedSignal, toSignal stables ; *ngIf déprécié ; zoneless stable en 20.2 |
| 21.0 | 19 novembre 2025 | zoneless par défaut ; Signal Forms ; Vitest |
| 22.0 | 3 juin 2026 | OnPush par défaut ; Signal Forms et resource stables ; @Service |
| 22.1 | 29 juillet 2026 | option set de linkedSignal ; migration vers @Service |
Angular 17 et 18 : un nouveau langage de template
Angular 17 remplace les directives structurelles par des blocs du langage de template : @if, @for, @switch et @defer, tous en developer preview, stables en 18. Le changement dépasse la syntaxe : @for exige un track que *ngFor laissait facultatif, et le cours Bases dit ce que les blocs apportent. Le composant d'avant compile toujours en 22.1, mais ses directives sont dépréciées depuis la 20.
// Angular 16 et avant : directives structurelles importees d'@angular/common.
// NgIf, NgForOf et NgSwitch sont deprecies depuis Angular 20.
import { Component } from '@angular/core';
import { NgFor, NgIf } from '@angular/common';
interface Tache {
id: number;
titre: string;
}
@Component({
selector: 'app-taches',
imports: [NgIf, NgFor],
template: `
<ul *ngIf="taches.length > 0; else vide">
<li *ngFor="let tache of taches; trackBy: parId">{{ tache.titre }}</li>
</ul>
<ng-template #vide><p>Aucune tâche.</p></ng-template>
`,
})
export class Taches {
taches: Tache[] = [{ id: 1, titre: 'Relire le contrat' }];
parId = (_: number, tache: Tache) => tache.id;
}// Angular 17 (stable en 18) : le flux de controle fait partie du langage de
// template. Rien a importer, et track est obligatoire.
import { Component, signal } from '@angular/core';
interface Tache {
id: number;
titre: string;
}
@Component({
selector: 'app-taches',
template: `
@if (taches().length > 0) {
<ul>
@for (tache of taches(); track tache.id) {
<li>{{ tache.titre }}</li>
}
</ul>
} @else {
<p>Aucune tâche.</p>
}
`,
})
export class Taches {
protected readonly taches = signal<Tache[]>([{ id: 1, titre: 'Relire le contrat' }]);
}
// Les deux versions rendent le meme DOM : <ul><li>Relire le contrat</li></ul>.
// ng generate @angular/core:control-flow reecrit le template de la premiere en
// blocs ; le passage du champ a un signal reste a faire a la main. La même version rend stables signal et computed, laisse effect en developer preview, et fait du builder fondé sur esbuild et Vite le défaut des projets neufs, que ng new crée désormais en standalone. Les mineures suivantes apportent les entrées et sorties en signals, en developer preview : input() en 17.1, model() et viewChild() en 17.2, output() en 17.3.
Angular 18 stabilise le flux de contrôle et @defer, et livre le mode sans Zone.js, encore expérimental, sous le nom provideExperimentalZonelessChangeDetection. Il déprécie HttpClientModule au profit de provideHttpClient, et la 18.1 ajoute @let, en developer preview. Le cours Performance traite @defer, ainsi que la relecture d'événements du rendu serveur, apparue en 18.
Angular 19 et 20 : les signals deviennent la norme
Angular 19 inverse un défaut : un composant, une directive ou un pipe est standalone sans le dire, et standalone: false désigne l'exception. La même version rend stables input, output, model, les requêtes en signals et @let. Les décorateurs @Input et @Output restent pris en charge, mais la forme en signals supprime ngOnChanges : une valeur dérivée d'une entrée est un computed, exact à chaque instant sans être recalculée à la main.
// Avant Angular 17.1 : decorateurs, et ngOnChanges pour deriver un etat.
import { Component, EventEmitter, Input, OnChanges, Output } from '@angular/core';
@Component({
selector: 'app-quantite',
template: `
<button (click)="changer(-1)">-</button> {{ quantite }} / {{ stock }}
<button (click)="changer(1)" [disabled]="epuise">+</button>
`,
})
export class Quantite implements OnChanges {
@Input({ required: true }) stock!: number;
@Input() quantite = 1;
@Output() quantiteChange = new EventEmitter<number>();
epuise = false;
ngOnChanges(): void {
this.epuise = this.quantite >= this.stock;
}
changer(pas: number): void {
this.quantite += pas;
this.epuise = this.quantite >= this.stock; // a recalculer ici aussi
this.quantiteChange.emit(this.quantite);
}
}// input() (17.1) et model() (17.2), stables en 19 : ce sont des signals, un
// computed suffit, et [(quantite)] fonctionne sans sortie ecrite a la main.
import { Component, computed, input, model } from '@angular/core';
@Component({
selector: 'app-quantite',
template: `
<button (click)="changer(-1)">-</button> {{ quantite() }} / {{ stock() }}
<button (click)="changer(1)" [disabled]="epuise()">+</button>
`,
})
export class Quantite {
readonly stock = input.required<number>();
readonly quantite = model(1);
protected readonly epuise = computed(() => this.quantite() >= this.stock());
protected changer(pas: number): void {
this.quantite.update((q) => q + pas);
}
}
// Avec stock = 2, un clic sur + affiche « - 2 / 2 + » et desactive le bouton. La 19 introduit aussi linkedSignal, en developer preview, resource, expérimental, et afterRenderEffect, expérimental puis stable en 20.0. Elle livre l'hydratation incrémentale, que la feuille de route d'angular.dev dit en developer preview alors que withIncrementalHydration est marqué @experimental dans les déclarations de la 19 : c'est le marqueur qui fait foi. La 19.2 ajoute httpResource, expérimental.
Angular 20 est la version de la stabilisation. Le CHANGELOG de la 20.0 rend stables effect, linkedSignal, toSignal, toObservable, PendingTasks (sa méthode run reste en developer preview en 22.1.7) et l'hydratation incrémentale, et renomme afterRender en afterEveryRender. Le mode zoneless passe en developer preview sous son nom définitif, provideZonelessChangeDetection, puis devient stable en 20.2, le 20 août 2025. *ngIf, *ngFor et *ngSwitch sont dépréciés. Côté outils, les schémas cessent de suffixer les fichiers : ng generate component panier crée panier.ts et non plus panier.component.ts ; Vitest arrive, expérimental.
Le langage d'expression des templates, longtemps un sous-ensemble étroit de JavaScript, se rapproche de TypeScript version après version, de la 19.2 à la 21.2.
import { Component, signal } from '@angular/core';
interface Ligne {
reference: string;
prix: number;
remise?: number;
}
// Chaque ligne du template est refusee par le compilateur avant la version indiquee.
@Component({
selector: 'app-expressions',
template: `
<p>{{ \`\${lignes().length} lignes\` }}</p> <!-- 19.2 -->
<p>{{ 2 ** 10 }} octets</p> <!-- 20.0 -->
<p>{{ 'remise' in lignes()[0] ? 'remisé' : 'plein tarif' }}</p> <!-- 20.0 -->
<p>{{ /^[A-Z]{3}-[0-9]+$/.test(code()) }}</p> <!-- 21.0 -->
<p>{{ [...lignes(), autre].length }}</p> <!-- 21.1 -->
<p>{{ lignes().map((l) => l.prix).join(' + ') }}</p> <!-- 21.2 -->
`,
})
export class Expressions {
protected readonly lignes = signal<Ligne[]>([{ reference: 'USB-64', prix: 12 }]);
protected readonly code = signal('SSD-1');
protected readonly autre: Ligne = { reference: 'SSD-1T', prix: 90, remise: 10 };
}
// Rendu : 1 lignes | 1024 octets | plein tarif | true | 2 | 12Angular 21 : les défauts changent
Angular 21 ne crée plus d'application avec Zone.js : ng new produit une application zoneless, que les notifications des signals suffisent à rafraîchir. Une application qui repose encore sur Zone.js doit désormais le déclarer par provideZoneChangeDetection : selon le CHANGELOG de la 21.0, Angular ne fournit plus par défaut l'ordonnanceur qui l'exploite. Vitest devient stable et configuré pour les projets neufs, à la place de Karma, et HttpClient s'injecte sans provideHttpClient, fourni à la racine.
// src/app/app.config.ts tel que l'ecrit ng new --zoneless=false
// (@schematics/angular 22.1.8). Depuis la 21, le mode Zone.js se declare : le
// schema ajoute aussi zone.js aux dependances et aux polyfills d'angular.json.
import {
ApplicationConfig,
provideBrowserGlobalErrorListeners,
provideZoneChangeDetection,
} from '@angular/core';
import { provideRouter } from '@angular/router';
import { routes } from './app.routes';
export const appConfig: ApplicationConfig = {
providers: [
provideBrowserGlobalErrorListeners(),
provideZoneChangeDetection({ eventCoalescing: true }),
provideRouter(routes),
],
}; La 21 livre aussi Signal Forms, dans @angular/forms/signals, expérimental, et Angular Aria, un paquet de composants accessibles sans style, en developer preview. La 21.2 ajoute ChangeDetectionStrategy.Eager, un alias de Default, dont le nom annonce le changement de la version suivante. Le cours Signals décrit ce que le mode zoneless exige d'un composant ; le cours Performance, ce que cela coûte.
Angular 22 et 22.1 : OnPush par défaut, des API stables
Angular 22 achève le trajet : un composant qui ne précise pas sa stratégie est OnPush. ng update écrit Eager sur les composants qui n'avaient rien déclaré, et remplace Default, déprécié depuis la 21.2, pour que leur comportement ne change pas. Une application neuve en 22.1 ne porte plus aucune trace des anciens défauts.
Ce défaut ne devenait raisonnable qu'après les signals et le mode zoneless : un composant dont l'état affiché tient dans des signals et des entrées, ce qu'exige déjà le zoneless, se rafraîchit en OnPush comme avant, et seuls les sous-arbres que rien n'a marqués cessent d'être parcourus. Le cours Performance dit ce qui marque une vue.
// Ce que genere ng new en 22.1 (@schematics/angular 22.1.8, options par
// defaut), deux fichiers reunis ici.
// src/app/app.config.ts : ni provideZoneChangeDetection, ni zone.js dans les
// polyfills d'angular.json.
import { ApplicationConfig, provideBrowserGlobalErrorListeners } from '@angular/core';
import { provideRouter } from '@angular/router';
import { routes } from './app.routes';
export const appConfig: ApplicationConfig = {
providers: [provideBrowserGlobalErrorListeners(), provideRouter(routes)],
};
// src/app/app.ts : ni standalone: true (defaut depuis 19), ni changeDetection
// (OnPush par defaut depuis 22), ni suffixe .component (depuis 20).
import { Component, signal } from '@angular/core';
import { RouterOutlet } from '@angular/router';
@Component({
imports: [RouterOutlet],
selector: 'app-root',
templateUrl: './app.html',
styleUrl: './app.css',
})
export class App {
protected readonly title = signal('appli-neuve');
} La 22 rend stables Signal Forms, resource, rxResource (cours RxJS), httpResource et Angular Aria. Elle ajoute @Service, qui écrit en un mot le service fourni à la racine (le cours État applicatif s'en sert), injectAsync, qui charge un service à la demande par un import dynamique, et deux API expérimentales : debounced, qui rend une version retardée d'un signal, et les outils WebMCP, provideExperimentalWebMcpTools et declareExperimentalWebMcpTool, qui exposent des actions de l'application à un agent d'IA. HttpClient passe par fetch et non plus par XMLHttpRequest, l'hydratation incrémentale devient le défaut du rendu serveur, et avec elle la relecture d'événements (cours Performance), strictTemplates est actif sans être écrit, et TypeScript 6.0 est requis.
// @Service (22.0) equivaut a @Injectable({ providedIn: 'root' }), a une
// difference pres : l'injection par le constructeur y est refusee a la compilation
// (erreur NG2028), inject() seul.
// resource (experimental en 19.0, stable en 22.0) expose un chargement
// asynchrone en signals, et le relance quand params change.
import { Component, Service, inject, resource, signal } from '@angular/core';
interface Produit {
reference: string;
stock: number;
}
@Service()
export class Stocks {
private readonly donnees = new Map([
['USB-64', 12],
['SSD-1T', 0],
]);
async lire(reference: string, signal: AbortSignal): Promise<Produit> {
await new Promise((fin) => setTimeout(fin, 300)); // simule un appel distant
signal.throwIfAborted();
const stock = this.donnees.get(reference);
if (stock === undefined) throw new Error(`${reference} inconnu`);
return { reference, stock };
}
}
@Component({
selector: 'app-stock',
template: `
<input #champ [value]="reference()" (change)="reference.set(champ.value)" />
@if (produit.error()) {
<p>Erreur : {{ produit.error()?.message }}</p>
} @else if (produit.hasValue()) {
<p>{{ produit.value().reference }} : {{ produit.value().stock }} en stock</p>
} @else if (produit.isLoading()) {
<p>Chargement…</p>
}
`,
})
export class Stock {
private readonly stocks = inject(Stocks);
protected readonly reference = signal('USB-64');
protected readonly produit = resource({
params: () => this.reference() || undefined,
loader: ({ params, abortSignal }) => this.stocks.lire(params, abortSignal),
});
}
// Rendu : « Chargement… », puis « USB-64 : 12 en stock ».
// Saisir XYZ : « Erreur : XYZ inconnu ». En statut d'erreur, lire value() lève une exception ; l'exemple ne la lit donc que derrière hasValue(), qui rend false dans ce cas. Le cours Signals détaille les statuts, et le cours HttpClient, httpResource. Signal Forms, stable dans la même version, a son cours, Formulaires ; l'exemple suivant en montre la forme minimale.
// Signal Forms : experimental en 21.0, stable en 22.0 (cours Formulaires).
import { Component, signal } from '@angular/core';
import { email, form, FormField, required } from '@angular/forms/signals';
@Component({
selector: 'app-inscription',
imports: [FormField],
template: `
<input type="email" [formField]="inscription.email" />
@for (erreur of inscription.email().errors(); track erreur.kind) {
<p>{{ erreur.message }}</p>
}
<button [disabled]="inscription().invalid()">S'inscrire</button>
`,
})
export class Inscription {
private readonly modele = signal({ email: '' });
protected readonly inscription = form(this.modele, (chemin) => {
required(chemin.email, { message: 'Adresse obligatoire' });
email(chemin.email, { message: 'Adresse invalide' });
});
}
// Champ vide : « Adresse obligatoire », bouton desactive.
// « amar@ » : « Adresse invalide ». « [email protected] » : bouton actif. La 22.1 ajoute à linkedSignal une option set, qui intercepte chaque écriture et décide de ce qui est réellement écrit. Le calcul, lui, n'y passe pas : quand le stock tombe à zéro, la valeur recalculée est posée telle quelle.
// linkedSignal : apparu en 19.0, stable en 20.0 ; l'option set arrive en 22.1.
// Execute sous Node 24, hors de tout composant : un signal n'a pas besoin d'Angular
// pour etre lu ou ecrit.
import { linkedSignal, signal } from '@angular/core';
const stock = signal(5);
// Recalcule a chaque changement de stock, ecrivable entre-temps. L'option set
// intercepte chaque ecriture : la quantite ne depasse jamais le stock.
const quantite = linkedSignal(() => Math.min(1, stock()), {
set: (valeur, ecrire) => ecrire(Math.max(0, Math.min(valeur, stock()))),
});
quantite.set(3);
console.log(quantite()); // 3
quantite.update((q) => q + 10);
console.log(quantite()); // 5
stock.set(0);
console.log(quantite()); // 0 La même mineure fournit la migration ng generate @angular/core:service, qui remplace @Injectable({ providedIn: 'root' }) par @Service(). Elle laisse de côté les classes qui reçoivent leurs dépendances par le constructeur : le compilateur refuse l'injection par constructeur dans une classe @Service, où seul inject() est admis. Les exemples des autres cours, hors État applicatif, gardent @Injectable({ providedIn: 'root' }), que la 22.1 ne déprécie pas : pour une classe qui injecte par inject(), les deux formes fournissent le même service à la racine. La 22.1 déprécie aussi JSONP, que ses déclarations disent exposé aux failles XSS.
Retraits et dépréciations
La politique de dépréciation garde une API dépréciée au moins une majeure ; le retrait n'arrive que dans une majeure, avec, le plus souvent, une migration. Plusieurs dépréciations de la période sont encore en sursis dans la 22.1.7, d'autres ont abouti.
| API | Dépréciée | État en 22.1.7 | À la place |
|---|---|---|---|
| *ngIf, *ngFor, *ngSwitch | 20.0 | présentes | @if, @for, @switch (migration control-flow) |
| HttpClientModule | 18.0 | présent | provideHttpClient, ou rien depuis 21 |
| withFetch() | 22.0 | présent, superflu | rien : fetch est le moteur par défaut |
| ChangeDetectionStrategy.Default | 21.2 | présent, égal à Eager | Eager |
| withIncrementalHydration() | 22.0 | présent | rien : c'est le défaut |
| TestBed.flushEffects() | 20.0 | présent | TestBed.tick() (cours Tests) |
| afterRender | renommé en 20.0 | retiré | afterEveryRender |
| ChangeDetectorRef.checkNoChanges() | 17.0 | retiré en 22.0 | fixture.detectChanges() dans les tests |
| ComponentFactoryResolver | 13.2 | retiré en 22.0 | la classe du composant, passée directement |
Parmi les changements cassants de la 22 qui ne retirent aucune API, deux touchent presque tout projet. Dans un template, un chaînage optionnel sur une valeur nulle rend undefined, comme en TypeScript, et non plus null. Et un composant sans stratégie déclarée change de comportement, ce que la migration neutralise. La liste des migrations livrées avec le paquet dit ce que ng update réécrit.
# Les migrations que ng update applique en passant a la 22, lues dans
# node_modules/@angular/core/schematics/migrations.json (22.1.7) :
node -e "const m = require('./node_modules/@angular/core/schematics/migrations.json').schematics; for (const [nom, s] of Object.entries(m)) console.log(nom.padEnd(28), s.description)"
change-detection-eager Adds `ChangeDetectionStrategy.Eager` to all components.
http-xhr-backend Adds 'withXhr' to 'provideHttpClient' function calls when the 'HttpXhrBackend' is used. For more information see: https://angular.dev/api/common/http/withXhr
strict-templates-default Adds 'strictTemplates: false' in tsconfig.json when not set.
can-match-snapshot-required Adds the required third argument to canMatch callsites.
incremental-hydration Adds withNoIncrementalHydration() opt out to provideClientHydration() when incremental hydration is not enabled to retain pre-v22 behavior-.
strict-safe-navigation-narrow Disables the 'nullishCoalescingNotNullable & optionalChainNotNullable extended diagnostics.
model-output Migrate broken duplicate outputs
safe-optional-chaining Wraps optional chaining expressions in $safeNavigationMigration().À adopter, à connaître
Le critère est le statut, puis le coût d'une migration. Une API stable qui remplace une forme dépréciée s'adopte dans tout code neuf, et se migre dans le code existant quand un schéma le fait sans risque. Une API expérimentale se connaît : elle peut changer dans un correctif.
| Nouveauté | Statut en 22.1.7 | Avis |
|---|---|---|
| @if, @for, @switch, @let | stable depuis 18 (@let : 19) | à adopter ; migration automatique |
| input(), output(), model(), viewChild() | stable depuis 19 | à adopter ; migrations signal-input, outputs, signal-queries |
| effect, linkedSignal, toSignal | stable depuis 20 | à adopter, effect avec parcimonie (cours Signals) |
| Zoneless | stable depuis 20.2, défaut depuis 21 | à adopter ; demande un état en signals |
| resource, httpResource | stable depuis 22 | à adopter pour lire ; jamais pour écrire |
| Signal Forms | stable depuis 22 | à adopter pour un formulaire neuf ; l'existant migre champ par champ (cours Formulaires) |
| @Service, injectAsync | stable depuis 22 | @Service à adopter ; injectAsync à connaître |
| debounced, outils WebMCP | expérimental | à connaître |
Un projet ancien n'a pas à tout réécrire d'un coup. Les migrations automatiques de la colonne Avis sont des schémas de @angular/core, qui s'appliquent à un dossier à la fois par leur option path : ng generate @angular/core:control-flow pour les templates, puis ng generate @angular/core:signals, qui combine les migrations des entrées, des sorties et des requêtes.