Introduction
Publié le 26 mai 2025, Angular 20 marque une avancée importante pour le framework.
Cette version introduit plusieurs évolutions notables, comme les redirections asynchrones, le partage de données entre resolvers parents et enfants, ainsi que le support natif de l’opérateur d’exponentiation et des tagged template literals dans les templates.
Autre ajout marquant : le style guide officiel a été mis à jour afin d’encourager des pratiques plus cohérentes et maintenables à l’échelle des projets.
Dans cet article, nous explorons les nouveautés les plus significatives à travers des cas d’usage concrets.
Opérateur d'exponentiation
L'opérateur d'exponentiation **
est désormais pris en charge dans les templates. Cet opérateur, introduit en JavaScript avec ECMAScript 2016 (ES7), permet de simplifier les calculs de puissances directement dans vos expressions. Il fallait recourir à des méthodes comme Math.pow()
pour effectuer des calculs de puissances dans les templates, ce qui alourdissait le code et réduisait leur lisibilité. Avec l'opérateur **
, les calculs mathématiques deviennent plus intuitifs et lisibles. Voici quelques exemples concrets pour illustrer son utilisation.
Calcul simple
<p>
Avant : Math.pow(base, exponent) = {{ Math.pow(base, exponent) }}
</p>
<p>
Aujourd'hui : {{ base }} ** {{ exponent }} = {{ base ** exponent }}
</p>
export class ExponentialOperator {
base = 2;
exponent = 3;
protected readonly Math = Math;
}
Parenthèses obligatoires sur les opérateurs unaires lorsqu'il sont à la base du calcul
<p>(-2) ** {{ b }} = {{ (-2) ** b }}</p>
<p>(-6) ** {{ c }} = {{ (-6) ** c }}</p>
export class ExponentialOperator {
b = 3;
c = 4;
}
Calcul où l’opérateur **
est associatif
<p>{{ base }} ** {{ exponent }} ** {{ a }} = {{ base ** exponent ** a }}</p>
<p>{{ base }} ** ({{ exponent }} ** {{ a }}) = {{ base ** (exponent ** a) }}</p>
export class ExponentialOperator {
base = 2;
exponent = 3;
a = 2;
}
Base de calcul avec un chiffre négatif
<p>{{ negativeBase }} ** {{ b }} = {{ negativeBase ** b }}</p>
<p>{{ negativeBase }} ** {{ c }} = {{ negativeBase ** c }}</p>
export class ExponentialOperator {
negativeBase = -2;
b = 3;
c = 4;
}
Lien vers la Pull Request de l'opérateur d'exponentiation
Lien vers la Pull Request de fix de l'opérateur d'exponentiation
Tagged template literal
Encore une nouvelle feature côté template, cette fois-ci Angular introduit la prise en charge des tagged template literal. Un peu comme une extension des template literals. Ils permettent d'appliquer une fonction spécifique à un template literal pour le transformer ou le traiter avant qu'il ne soit utilisé. Voici un exemple simple pour illustrer comment ils fonctionnent.
@Component({
selector: 'app-tag-template-literal',
imports: [
FormsModule
],
template: `
<div>
<div>
<input type="number" [(ngModel)]="quantity" id="quantity"/>
@let options = ['Banane', 'Pomme', 'Bière', 'Tank', 'Saxophone'];
<select [(ngModel)]="product" id="greeting" name="greeting">
@for (option of options; track option) {
<option [ngValue]="option">{{ option }}</option>
}
</select>
</div>
<p>{{ tag\`Bonjour, \${quantity()} x \${product()} please !\` }}</p>
</div>
`
})
export class TaggedTemplateLiteral {
product = signal('Banane');
quantity = signal(10);
tag(strings: TemplateStringsArray, quantity: number, product: string) {
return `${strings[0]}${quantity}${strings[1]}${product}${strings[2]}`;
}
// Autre possibilité 👇
// tag(strings: TemplateStringsArray, ...values: any[]) {
// return `${strings[0]}${values[0]}${strings[1]}${values[1]}${strings[2]}`;
// }
}
On peut remarquer que, dans le cas des tagged template literals, les parenthèses ne sont pas nécessaires pour appeler la fonction. Au lieu de cela, la fonction est directement invoquée en utilisant les backticks (`
). Lorsqu'une fonction est utilisée comme un "tag" pour un template literal, elle reçoit automatiquement deux types d'arguments :
- Une liste de chaînes de caractères : Le premier argument est un tableau contenant toutes les parties statiques du template (le texte qui n'est pas interpolé). Ces chaînes de caractères sont les segments du template qui entourent les expressions dynamiques.
- Toutes les expressions interpolées : Les arguments suivants sont les valeurs des expressions dynamiques (celles qui sont placées à l'intérieur des
${}
dans le template).
Si on reprend l’exemple ci-dessus, on retrouve les arguments suivants :
- Le tableau
strings
contient les parties statiques du template : "Bonjour, ", " x ", et " please !". - Les valeurs dynamiques
${quantity}
et${product}
sont collectées à partir du tableauvalues
ou indiquées explicitement en arguments de la méthode : "10" et "Banane".
Lien vers la Pull Request Tagged template literal
État des features
Angular 20 marque une étape importante dans l'évolution du framework en stabilisant plusieurs API et fonctionnalités. Voici une vue d'ensemble des principales API stabilisées, accompagnée d'explications détaillées et d'analogies pour mieux comprendre leur utilité.
linkedSignal
La linkedSignal
API permet de créer des signaux réactifs qui sont automatiquement synchronisés avec une source de données externe. N'hésite pas à aller voir l'article sur la version 19 pour plus de détails.
Lien vers la Pull Request LinkedSignal
REQUEST
, RESPONSE_INIT
et REQUEST_CONTEXT
Ces tokens d'injection sont apparus dans la version 19 et permettent d'accéder aux objets de requête et de réponse pour les applications utilisant le rendu côté serveur (SSR) :
REQUEST
: fournit un accès à l'objet de requête HTTP actuel, tel qu'il est reçu par le serveur. Cela permet d'accéder aux en-têtes, à l'URL, aux cookies et à d'autres informations pertinentes de la requêteREQUEST_CONTEXT
: permet de transmettre des métadonnées personnalisées tout au long du cycle de vie de la requête. Il est particulièrement utile pour partager des informations spécifiques à la requête, comme le rôle de l'utilisateur ou la région.RESPONSE_INIT
: offre un moyen de configurer la réponse HTTP avant qu'elle ne soit envoyée au client. Cela inclut la définition d'en-têtes, le statut de la réponse et d'autres paramètres initiaux.
Lien vers la Pull Request REQUEST, REQUEST_CONEXT et RESPONSE_INIT
PendingTasks
PendingTasks
est un service qui permet de suivre les tâches asynchrones personnalisées afin d'informer Angular que l'application n'est pas encore stable.
Lien vers la Pull Request PendingTasks
AfterRenderRef
, AfterRenderOptions
, afterNextRender
et afterEveryRender
afterNextRender
: permet d'enregistrer un ou plusieurs callbacks qui seront exécutés une seule fois après le prochain cycle de détection des changements.AfterRenderRef
: Lors de l'enregistrement d'un callback avecafterNextRender
, une instance deAfterRenderRef
est retournée. Cette référence permet de gérer manuellement le cycle de vie du callback, notamment en le nettoyant explicitement si nécessaire.AfterRenderOptions
: L'interfaceAfterRenderOptions
permet de configurer le comportement des callbacks enregistrés avecafterNextRender
.afterEveryRender
: anciennementafterRender
, il permet d’exécuter une fonction après chaque rendu du composant.
⚠️ Aucun schematic n’a été fourni pour accompagner ces changements, ce qui implique une migration manuelle dans les projets existants.
Lien vers la Pull Request AfterRenderRef, AfterRenderOptions et afterNextRender
withI18nSupport()
Par défaut, lors de l'hydratation, Angular ignore les blocs marqués avec l'attribut i18n
, ce qui entraîne leur re-rendu complet côté client. Cela peut provoquer des décalages de mise en page et des problèmes de performance. La fonction withI18nSupport()
permet à Angular de prendre en charge ces blocs i18n lors de l'hydratation, assurant une transition fluide entre le rendu serveur et le client.
Lien vers la Pull Request withI18nSupport()
toObservable
La fonction toObservable()
permet de créer un RxJS Observable
à partir d'un Signal
.
Lien vers la Pull Request toObservable
toSignal
La fonction toSignal()
permet de créer un Signal
à partir d'un RxJS Observable
.
Lien vers la Pull Request toSignal
Zoneless Change Detection
Zoneless Change Detection
passe en "developer preview", c'est le point d’entrée officiel pour la détection de changements sans Zone.js ! Traditionnellement, Angular utilise Zone.js pour intercepter les opérations asynchrones (comme les événements DOM, les requêtes HTTP ou les timers) et déclencher automatiquement la détection des changements. Bien que pratique, cette méthode peut entraîner des surcoûts en termes de performances et de complexité de déboggage. Avec Zoneless Change Detection
, Angular abandonne sa dépendance à Zone.js. Désormais, les développeurs doivent informer explicitement Angular lorsqu'un changement nécessite une mise à jour de l'interface utilisateur. Les providers ont été renommés de provideExperimentalZonelessChangeDetection
à provideZonelessChangeDetection
.
Lien vers la Pull Request Zoneless change detection
*ngIf
, *ngFor
et *ngSwitch
Après de nombreuses versions au service des développeurs, les directives historiques *ngIf
, *ngFor
et *ngSwitch
sont désormais dépréciées. Elles devraient être supprimées d’ici Angular 22, soit dans environ un an.

*ngIf
dans Angular 20.Lien vers la Pull Request de dépréciation des directives
flushEffect
La méthode TestBed.flushEffects()
, introduite en Angular 17, est désormais dépréciée et cède sa place à TestBed.tick()
. Cette nouvelle méthode exécute l’ensemble du processus de synchronisation d’Angular contrairement à flushEffects()
, qui forçait l’exécution des effets de manière isolée. tick()
reproduit fidèlement le comportement d’une application Angular en cours d’exécution, ce qui améliore la précision et la pertinence des tests.
Lien vers la Pull Request de remplacement de flushEffects par tick
Lien vers la Pull Request d'introduction de tick
Resources
Avec la sortie d’Angular 20, le framework continue d’évoluer en introduisant des améliorations notables à l'API httpResource
. Initialement proposée en version expérimentale dans Angular 19.2 , cette API vise à simplifier la gestion des requêtes HTTP en s'intégrant harmonieusement avec les concepts modernes du framework, tels que les signaux et les ressources. Initialement, le premier argument de httpResource
pouvait recevoir une fonction retournant une valeur réactive ou une URL statique. Ce dernier n'est plus possible :
❌ Ancien usage (non réactif) – plus valable
readonly httpResource = httpResource<NoIdea>('/i_dont_know');
✅ Nouveau usage (réactif)
readonly userId = signal(666);
readonly userResource = httpResource<User>(() => `/users/${userId()}`);
Le second argument de httpResource
est un objet d'options typé HttpResourceOptions
, qui permet de contrôler le comportement de la ressource. Voici ses principales propriétés :
defaultValue
:T
La valeur retournée par lahttpResource
lorsqu'elle est en état d'inactivité, d'erreur ou de chargement.equal?: (a: T, b: T) => boolean
Si la fonctionequal
estime que les résultats sont équivalents, Angular considère que le signal n’a pas changé : il ne le marque donc pas comme « dirty », ce qui évite un nouveau cycle de détection des changements et prévient toute mise à jour inutile du template.parse?: (response: unknown) => T
Permet de transformer la réponse HTTP avant qu’elle soit renvoyée et utilisée par lahttpResource
.injector?: Injector
Par défaut l'injector du composant est utilisé, la resource est automatiquement nettoyé à la destruction du composant.
Exemple complet avec options :
readonly injector = inject(Injector);
readonly pokemonId = signal(1);
readonly resource = httpResource<Pokemon>(
() => this.pokemonId() ? `https://tyradex.vercel.app/api/v1/pokemon/${this.pokemonId()}` : undefined,
{
defaultValue: { ... },
parse: (response) => ({ ... }),
equal: (a, b) => {
const isEqual = a?.pokedex_id == b?.pokedex_id;
console.log('isEqual', a?.pokedex_id, b?.pokedex_id, isEqual);
return isEqual;
},
injector: this.injector
}
);
Concernant la gestion des erreurs dans httpResource
, celle-ci était relativement rudimentaire. En cas d’échec de la requête, l’erreur capturée dans le signal associé à la ressource était de type unknown
. Cela obligeait les développeurs à caster manuellement l’erreur ou à écrire du code défensif pour savoir s’il s’agissait bien d’une instance de Error
, ce qui n'était ni pratique ni sûr. Grâce à la PR #60610, la signature du signal d’erreur a été mise à jour pour renvoyer désormais une valeur typée Error | undefined
. Ce que ça change concrètement :
- Plus besoin de faire un check de type avec
instanceof Error
. - Le signal est explicitement
undefined
tant qu’aucune erreur n’est survenue. - On bénéficie d’un typage fort dès la lecture du signal.
constructor() {
effect(() => {
const error = this.resource.error(); // type: Error | undefined
if (error) {
console.error('Erreur détectée :', error.message);
}
});
}
De plus, la méthode reload()
a été déplacée dans la sous-classe WritableResource
, ce qui signifie que seules les ressources mutables peuvent être rechargées.
Côté resource
et rxResource
, quelques renommages notables à prendre en compte :
request
a été renommée enparams
dans les options à fournir aux deux API.loader
a été renommée enstream
dansrxResource
.
⚠️ Là encore, aucun schematic n’automatise cette transition.
Lien vers la Pull Request du throw d'erreur pour `httpResource`
Lien vers la Pull Request d'ajout de la fonction equal
dans `httpResource`
Lien vers la Pull Request de `httpResource`
Nouveau mot clé in
pour les expressions
Le langage des templates s'enrichit une nouvelle fois en prenant en charge l'opérateur JavaScript in
, permettant de vérifier si une propriété existe dans un objet directement, même si sa valeur est undefined
ou null
.
const user = {
name: undefined,
email: null
};
'name' in user // ✅ true
'email' in user // ✅ true
'age' in user // ❌ false
Voici quelques cas d'utilisations :
1. Conditionner l’affichage avec @if
@if ('admin' in permissions) {
<button>Accès admin</button>
}
➡️ Le bouton est affiché uniquement si l’objet permissions
contient la clé "admin"
, quelle que soit sa valeur (true
, false
, null
, undefined
…).
2. Appliquer un style inline
<div [style.border]="'error' in result ? '1px solid red' : 'none'">
{{ result.message }}
</div>
➡️ On affiche une bordure rouge si une propriété error existe dans un objet result.
3. Boucler avec @for
et ajouter une classe selon la présence d’une propriété
@for (user of users) {
<div [class.online]="'status' in user && user.status === 'online'">
{{ user.name }}
</div>
}
➡️ Chaque utilisateur ayant un statut online
se voit appliquer une classe CSS spécifique.
Lien vers la Pull Request de keyword in
Création dynamique des composants
La méthode ViewContainerRef.createComponent()
, déjà disponible depuis Angular 14, permet de créer dynamiquement des composants pour les insérer manuellement. Elle acceptait déjà un second argument d’options mais deux nouvelles propriétés majeures ont été introduites : bindings
et directives
.
abstract createComponent<C>(componentType: Type<C>, options?: {
index?: number;
injector?: Injector;
ngModuleRef?: NgModuleRef<unknown>;
environmentInjector?: EnvironmentInjector | NgModuleRef<unknown>;
projectableNodes?: Node[][];
directives?: (Type<unknown> | DirectiveWithBindings<unknown>)[];
bindings?: Binding[];
}): ComponentRef<C>;
directives
: d’attacher dynamiquement des directives au composant créé.bindings
:inputBinding
: Permet de lier une propriété d'entrée (input
) du composant créé avec une valeur ou une fonction réactive.outputBinding
: Permet d’attacher une fonction de rappel à une propriété marquéeoutput
dans le composant créé. Le callback est invoqué automatiquement lors des émissions (emit
).twoWayBinding
: Crée une liaison bidirectionnelle entre une propriété du composant créé et unsignal
local.
Prenons maintenant un exemple concret pour illustrer ces nouveautés. Nous avons une application avec un bouton qui ouvre une modal, dont le contenu et le style sont entièrement pilotés dynamiquement.
Le composant parent : App
@Component({
selector: 'app-root',
imports: [],
template: `
<button (click)="openModal()">Open modal</button>
{{ data() }}
<ng-container #modal></ng-container>
`,,
styleUrl: './app.scss'
})
export class App {
title = signal('20');
data = signal('');
modalContainer = viewChild.required('modal', { read: ViewContainerRef });
openModal() {
if (!this.modalContainer().length) {
this.modalContainer().createComponent(Modal, {
bindings: [
inputBinding('title', () => `Angular ${this.title()} est là !`),
twoWayBinding('data', this.data),
outputBinding('close', result => {
console.log(result);
if (result) {
this.modalContainer().clear();
}
})
],
directives: [
{
type: TitleColorDirective,
bindings: [
inputBinding('clazz', () => !this.data() ? 'deeppink' : 'yellow')
]
}
]
})
}
}
}
inputBinding('title', ...)
injecte dynamiquement un titre personnalisé dans la modal.twoWayBinding('data', this.data)
permet de lier un champ de saisie de la modal avec une donnée dans le parent, automatiquement synchronisée.outputBinding('close', ...)
permet de réagir à la fermeture de la modal.
Le composant créé dynamiquement : Modal
@Component({
selector: 'app-modal',
imports: [FormsModule],
template: `
<h1>{{ title() }}</h1>
<input [(ngModel)]="data" name="data"/>
<button (click)="close.emit(true)">Youhou !</button>
<button (click)="close.emit(false)">D’accord.</button>
`,
styleUrl: './modal.scss'
})
export class Modal {
title = input.required<string>();
data = model<string>();
close = output<boolean>();
}
title
est un input réactif injecté dynamiquement.data
est synchronisé viatwoWayBinding()
.close
émet vers le parent au clic sur l'un des boutons.
La directive conditionnelle : TitleColorDirective
@Directive({
selector: '[titleColorDirective]',
host: {
'[class]': 'clazz()'
}
})
export class TitleColorDirective {
clazz = input.required<string>();
}
- La classe CSS appliquée au
<h1>
est dynamique selon le contenu dedata
. - Cette directive est injectée uniquement lorsque le composant est créé, via
directives
danscreateComponent()
.
Lien vers la Pull Request de support des directives pour la création de composants dynamiques
Lien vers la Pull Request de création de composants dynamiques
Style guide
Suppression des suffixes .component, .service, etc.
L’une des évolutions les plus visibles est l’abandon des suffixes explicites dans les noms de fichiers et de classes. Désormais :
- Ancienne convention : user-profile.component.ts → UserProfileComponent
- Nouvelle convention recommandée : user-profile.ts → UserProfile
Cette recommandation vise à alléger les noms et à favoriser la lisibilité. Elle s’applique également à d’autres entités Angular (.pipe, .directive, .service, etc.).
🔹 Bon à savoir : Il ne s’agit pas d’une obligation. Si vous souhaitez conserver l’ancienne convention, Angular 20 permet de le faire très simplement via les schematics.
Conserver les anciens noms via angular.json
Pour garder l’ancienne convention de nommage après la migration vers Angular 20, vous pouvez configurer les schematics dans angular.json
comme suit :
"schematics": {
...
"@schematics/angular:component": {
"type": "component"
},
"@schematics/angular:directive": {
"type": "directive"
},
"@schematics/angular:service": {
"type": "service"
},
"@schematics/angular:guard": {
"typeSeparator": "."
},
"@schematics/angular:interceptor": {
"typeSeparator": "."
},
"@schematics/angular:module": {
"typeSeparator": "."
},
"@schematics/angular:pipe": {
"typeSeparator": "."
},
"@schematics/angular:resolver": {
"typeSeparator": "."
}
}
Cette configuration sera appliquée automatiquement lors de la montée de version :

Nouvelle structure de projet : disparition du dossier app/
Le dossier app/, historiquement présent dans tous les projets Angular, disparaît au profit d’une organisation par domaines fonctionnels :
-src/app/
+src/features/
+src/shared/
+src/core/
Angular recommande une structure modulaire et orientée métier :
features/
: contient les fonctionnalités métiers de l’applicationshared/
: regroupe les composants ou modules réutilisablescore/
: contient les services globaux, guards, interceptors, etc.
🔍 Référence :
Angular v20 Style Guide – structure par fonctionnalités
Angular v19 Style Guide – structure 04-06
Nouveau style pour lier dynamiquement classes et styles CSS
Autre changement notable : Angular 20 recommande désormais l’utilisation directe de [style.X]
et [class.X]
dans les templates, plutôt que ngStyle
et ngClass
.
Avant :
<div [ngClass]="{ 'active': isActive }"></div>
<div [ngStyle]="{ color: color }"></div>
Maintenant recommandé :
<div [class.active]="isActive"></div>
<div [style.color]="color"></div>
Router
Resolver
Auparavant, il n’était pas possible d’accéder directement aux données résolues par un parent depuis un resolver enfant. Des discussions sur GitHub ont mis en évidence cette limitation, notamment dans l’issue #47287. Cette contrainte est désormais levée avec Angular 20 : les données résolues par une route parent sont automatiquement fusionnées dans le route.data des routes enfants. Ci-dessous un exemple :
export const routes: Routes = [
{
path: 'pokemons/:id',
resolve: { pokemon: pokemonsResolver },
children: [
{
path: 'types',
// 👇 Ici, le resolver `typesResolver` a maintenant accès à `route.data['pokemon']`
resolve: { types: typesResolver },
loadComponent: () => import('./http-resource/http-resource')
}
]
}
];
Dans cet exemple, pokemonsResolver
récupère un Pokémon via l’id de l’URL. Ensuite, la route enfant types utilise typesResolver
, qui peut accéder directement à route.data['pokemon']
pour charger dynamiquement les types associés au Pokémon.
export const typesResolver: ResolveFn<Type[]> = (route, state) => {
const pokemon = route.data['pokemon']; // disponible grâce au resolver parent
return inject(TypesService).getTypesForPokemon(pokemon.id);
};
Lien vers la Pull Request d'accès aux données du resolver parent
Redirection asynchrone
Jusqu’à Angular 19, les redirections dans la configuration des routes étaient limitées à des chaînes de caractères statiques ou un UrlTree :
{ path: '', redirectTo: 'home', pathMatch: 'full' }
ou
{ path: '', redirectTo: () => router.createUrlTree(['/home']), pathMatch: 'full' }
Il était impossible d’effectuer une redirection asynchrone directement dans la configuration des routes. Pour contourner cette limitation, il fallait passer par des guards (canActivate
) pour intercepter la navigation, ou déclencher un router.navigate()
dans un composant. Avec Angular 20, la fonction redirectTo accepte désormais un retour asynchrone :
type MaybeAsync<T> = T | Promise<T> | Observable<T>;
Et plus précisément :
redirectTo: () => MaybeAsync<string | UrlTree>
Exemple avec fonction async :
{
path: '',
redirectTo: async (): Promise<string> => {
const authService = inject(AuthService);
const status = await authService.checkAuthStatus();
return status.isLoggedIn ? 'dashboard' : 'login';
},
pathMatch: 'full'
}
Ici, la fonction redirectTo
effectue un appel asynchrone via AuthService
, et redirige dynamiquement vers 'dashboard' ou 'login'.
Conclusion
Angular 20 continue de faire évoluer le framework dans la bonne direction : plus de flexibilité, moins de contournements, et des APIs modernes pensées pour les développeurs.
La mise à jour du style guide officiel témoigne aussi d’une volonté d’harmoniser les pratiques de développement, en apportant clarté et cohérence aux projets Angular de toutes tailles.
Ces nouveautés ne sont pas de simples ajouts cosmétiques : elles traduisent une attention croissante portée à l’expérience développeur, à la lisibilité du code, et à la maintenabilité à long terme.
Que vous construisiez une application existante ou un nouveau projet from scratch, Angular 20 vous offre des fondations plus solides, plus intuitives et plus puissantes.
Il ne reste plus qu’à expérimenter, tester… et mettre à jour vos projets ! 🚀
📚 Pour aller plus loin
- 📺 Vidéo de présentation des nouveautés d'Angular : Youtube
- 🧭 Comparateur d’évolutions entre Angular 19 et 20 : CanIUse Angular – 19 vs 20
- 📘 Dernière version du style guide Angular : angular.dev/style-guide