Pourquoi $any() est un faux ami dans les templates Angular et comment le remplacer par l'optional chaining, les template reference variables et le narrowing.
Un crash en prod causé par un $any()
Vendredi, 17 h. Le tableau de bord est en production depuis dix minutes quand Sentry s'illumine : TypeError: Cannot read properties of null (reading 'name'). Le coupable ? Une seule ligne dans un template Angular :
<!-- profil.component.html -->
<h1>Bonjour {{ $any(user).name }}</h1>
Le développeur avait écrit $any(user) pour « faire taire le compilateur » qui se plaignait. Le build est passé. Les tests aussi. Et pourtant, dès qu'un utilisateur arrive sur la page avant que la requête HTTP n'ait renvoyé son profil, user vaut null, et l'application plante. $any() n'avait protégé de rien : il avait juste caché le problème jusqu'en production.
$any(...) dès qu'une erreur de typage apparaît dans un template, parce que c'est le moyen le plus rapide de « faire compiler ». C'est un très mauvais réflexe : relisez systématiquement le code généré et remplacez chaque $any() par l'outil adapté — optional chaining, template reference variable ou narrowing. Une suggestion qui compile n'est pas une suggestion qui est correcte.
Cet article démonte une idée reçue tenace : non, $any(user).name n'est pas un « raccourci pratique ». C'est un piège qui combine deux défauts. À l'inverse, l'optional chaining (?., ?.(), ?.[]) et les template reference variables résolvent réellement le problème : ils conservent la sécurité de type et protègent à l'exécution. Chaque section répond à la même question : « en quoi ça m'évite un bug ? »
À quoi sert vraiment $any() (et pourquoi il existe)
$any() est une fonction spéciale du langage de template Angular. Elle ne fait rien à l'exécution : elle se contente de dire au compilateur de templates « considère que cette expression est de type any, arrête de la vérifier ». C'est l'équivalent template du as any de TypeScript.
<!-- $any() désactive le type-checking sur l'expression -->
<p>{{ $any(data).champNonType }}</p>
Pourquoi cette fonction existe-t-elle ? Pour un cas légitime et rare : quand le typage est réellement impossible ou faux. Par exemple, l'interop avec une librairie JavaScript sans définitions de types, ou un faux positif du compilateur sur une structure dynamique. Dans ces situations, $any() est une soupape de secours volontaire et assumée.
Le problème n'est donc pas l'existence de $any(), mais son usage par défaut : l'utiliser comme une rustine dès qu'une erreur rouge apparaît dans l'éditeur. Car ce que beaucoup oublient, c'est que $any() n'a aucun effet runtime. Il ne crée pas de garde, ne vérifie pas la nullité, ne sécurise rien. Il dit juste « tais-toi » au compilateur.
$any() agit à la compilation, jamais à l'exécution. C'est la racine de toute la confusion. On croit gagner de la sécurité ; on perd en réalité la seule qu'on avait — celle du compilateur.
Le piège démontré : $any(user).name vs user?.name
Prenons le composant qui a planté en production. user peut être null le temps du chargement. Comparons les deux écritures, ligne à ligne.
// profil.component.ts
import { Component, signal } from '@angular/core';
interface User { name: string; }
@Component({
selector: 'app-profil',
standalone: true,
templateUrl: './profil.component.html',
})
export class ProfilComponent {
// user vaut null tant que la requête HTTP n'a pas répondu
user = signal<User | null>(null);
}
<!-- ❌ AVANT : $any() — le piège -->
<h1>Bonjour {{ $any(user()).name }}</h1>
<!-- 1. .naem passerait à la compilation (typage désactivé)
2. si user() est null -> CRASH runtime : Cannot read 'name' of null -->
<!-- ✅ APRÈS : optional chaining — la solution -->
<h1>Bonjour {{ user()?.name }}</h1>
<!-- 1. .name est vérifié : .naem provoque une erreur de compilation
2. si user() est null -> l'expression vaut undefined, AUCUN crash -->
Tableau comparatif : deux défauts contre zéro
| Critère | $any(user).name |
user?.name |
|---|---|---|
Faute de frappe (.naem) |
❌ Passe à la compilation | ✅ Erreur de compilation |
| Renommage de propriété refactoré | ❌ Non suivi (silencieux) | ✅ Suivi par l'IDE |
| Autocomplétion de l'éditeur | ❌ Perdue | ✅ Conservée |
Si user est null |
❌ Crash runtime | ✅ Court-circuit à undefined |
| Bug visible en… | ❌ Production | ✅ Compilation / dev |
$any() cumule deux défauts (pas de type safety + pas de sécurité runtime). ?. apporte deux garanties (type safety conservée + court-circuit propre). Ce n'est pas « légèrement mieux » : c'est l'opposé exact.
Le cas du $event.target : contourner le typage
Voici l'usage de $any() le plus répandu — et le plus instructif, parce qu'ici le problème n'est même pas un null, mais un typage trop large.
<!-- ❌ Le grand classique copié-collé partout -->
<input (input)="query.set($any($event.target).value)">
Pourquoi ce $any() ? Parce que $event.target est typé EventTarget par le DOM — une interface générique qui ne possède pas de propriété .value. Le compilateur a raison de refuser : tous les EventTarget ne sont pas des <input>. Ici, $any() ne masque pas un null : il masque le fait que vous affirmez sans preuve que la cible est un champ texte.
La solution idiomatique en Angular n'est pas de mentir au compilateur, mais de lui donner la bonne information dès la source — avec une template reference variable :
<!-- ✅ #q référence l'élément, typé HTMLInputElement -->
<input #q (input)="query.set(q.value)">
<!-- q.value est vérifié à la compilation : c'est bien un string.
Une faute (q.valeu) provoque une erreur, pas un crash silencieux. -->
La variable #q est, pour Angular, de type HTMLInputElement. Donc q.value est connu, typé string, autocomplété, et vérifié. Aucun $any(), aucune perte de sécurité, et le code est même plus court à lire.
q est l'input de recherche, là où $any($event.target) n'apprend rien sur la nature réelle de la cible.
Template reference variable vs cast as HTMLInputElement
On pourrait objecter : « je peux aussi caster côté composant ». Effectivement, beaucoup déplacent le problème dans la classe TypeScript :
// ❌ Le cast : une assertion qui peut MENTIR
onInput(event: Event): void {
// 'as HTMLInputElement' n'est PAS vérifié : c'est une promesse non tenue
const value = (event.target as HTMLInputElement).value;
this.query.set(value);
}
Le mot-clé as est une assertion de type : il dit au compilateur « crois-moi sur parole ». Si l'événement venait en réalité d'un autre élément (un <div> contenteditable, une délégation d'événement mal câblée), .value serait undefined à l'exécution — et le compilateur n'aurait rien vu. Le cast déplace le risque, il ne l'élimine pas.
<!-- ✅ La template reference variable : vérifiée, pas asserée -->
<input #q (input)="onInput(q.value)">
// onInput reçoit directement un string typé et vérifié
onInput(value: string): void {
this.query.set(value);
}
Pourquoi le #ref est supérieur au cast
| Aspect | as HTMLInputElement |
#q (template ref) |
|---|---|---|
| Nature | Assertion (promesse) | Type réel inféré par Angular |
| Vérifié à la compilation | ❌ Non (on fait confiance) | ✅ Oui |
| Peut mentir / casser au runtime | ❌ Oui si la cible diffère | ✅ Non, l'élément EST l'input |
| Lisibilité du template | Logique éclatée dans la classe | Intention claire et locale |
as est seulement cru. Préférez toujours ce qui est vérifié à ce qui est promis.
Appels de fonction et de méthode en toute sécurité
Le danger de $any() est encore plus net sur les appels. Avec $any(obj).fn(), le compilateur ne vérifie ni que obj existe, ni que fn existe, ni que sa signature est respectée. C'est trois portes ouvertes vers le crash. L'optional chaining offre un opérateur précis pour chaque cas.
?. — l'objet porteur peut être null
<!-- user peut être null : on court-circuite l'appel entier -->
<p>{{ user?.getName() }}</p>
<!-- Si user est null/undefined : l'expression vaut undefined.
getName() n'est même pas appelée. Aucun crash. -->
?.() — la méthode elle-même peut être absente
<!-- user existe, mais getName n'est peut-être pas défini -->
<p>{{ user.getName?.() }}</p>
<!-- ?.() n'appelle getName que si c'est bien une fonction.
Si getName est undefined : court-circuit, pas de "is not a function". -->
cb?.() — un callback @Input optionnel
// widget.component.ts
import { Component, input } from '@angular/core';
@Component({
selector: 'app-widget',
standalone: true,
// onSave est optionnel : le parent peut ne pas le fournir
template: `<button (click)="save()">Enregistrer</button>`,
})
export class WidgetComponent {
onSave = input<(() => void) | undefined>();
save(): void {
// ✅ cb?.() : appelé seulement si le parent a passé un callback
this.onSave()?.();
// ❌ $any(this.onSave)() crasherait si onSave est undefined
}
}
?.[] — accès indexé sur un tableau possiblement null
<!-- items peut être null, et le premier élément peut manquer -->
<span>{{ items?.[0]?.label }}</span>
<!-- Chaque maillon court-circuite : items null -> undefined,
tableau vide -> undefined. Jamais "Cannot read '0' of null". -->
Le contraste avec $any()
| Écriture | Vérifie l'existence ? | Vérifie la signature ? | Si absent ? |
|---|---|---|---|
$any(obj).fn() |
❌ Non | ❌ Non | 💥 Crash |
obj?.fn() |
✅ obj | ✅ fn | ↩️ undefined |
obj.fn?.() |
obj requis | ✅ fn | ↩️ undefined |
arr?.[i]?.x |
✅ arr + index | — | ↩️ undefined |
obj?.fn() court-circuite si obj est nul, tandis que obj.fn?.() court-circuite si la méthode est nulle. On peut combiner les deux — obj?.fn?.() — quand l'objet et la méthode sont incertains.
Le cas des unions : narrowing runtime
Parfois, mieux typer ne suffit pas : il faut valider la valeur à l'exécution. Prenons un tri dont la direction est une union stricte :
// table.component.ts
import { Component, signal } from '@angular/core';
type SortDir = 'asc' | 'desc' | '';
@Component({ /* ... */ })
export class TableComponent {
sort = signal<SortDir>('');
// ...
}
Si l'utilisateur choisit la direction via un <select>, la valeur brute lue est un string quelconque — pas un SortDir. Lui faire confiance (avec un cast ou un $any()) reviendrait à accepter n'importe quelle chaîne, y compris une valeur invalide qui casserait votre logique de tri plus loin.
<!-- ❌ La valeur brute n'est pas garantie d'être un SortDir -->
<select #s (change)="sort.set($any(s.value))"> ... </select>
<!-- $any masque que s.value pourrait être 'random' : aucune validation -->
<!-- ✅ On passe par un handler qui valide (narrowing runtime) -->
<select #s (change)="setSort(s.value)">
<option value="">Aucun</option>
<option value="asc">Croissant</option>
<option value="desc">Décroissant</option>
</select>
// Un petit type guard valide la valeur AVANT de l'accepter
private isSortDir(v: string): v is SortDir {
return v === 'asc' || v === 'desc' || v === '';
}
setSort(value: string): void {
// narrowing : on ne met le signal à jour que si la valeur est légale
if (this.isSortDir(value)) {
this.sort.set(value); // value est ici typé SortDir, garanti valide
}
// sinon : on ignore (ou on log) — l'état reste cohérent
}
La différence est de nature, pas de degré. $any() dirait « fais comme si c'était un SortDir » sans rien vérifier ; le type guard vérifie réellement à l'exécution que la valeur appartient à l'union. C'est donc plus sûr, pas seulement mieux typé : une valeur aberrante ne peut jamais entrer dans votre état.
string non fiables. Le narrowing runtime est la frontière qui empêche une valeur invalide de contaminer la suite de votre logique métier.
?? et court-circuit : compléter le ?.
Un point essentiel souvent mal compris : ?. court-circuite vers undefined, pas vers une exception et pas vers une chaîne vide. Concrètement, {{ user()?.name }} affiche une chaîne vide si user() est null (Angular n'affiche pas le mot « undefined »), mais la valeur de l'expression est bien undefined. Cela a deux conséquences pratiques.
1. Fournir une valeur par défaut avec ??
<!-- ?? (nullish coalescing) fournit un repli quand l'expression est null/undefined -->
<h1>Bonjour {{ user()?.name ?? 'invité' }}</h1>
<!-- user() null -> ?. donne undefined -> ?? renvoie 'invité' -->
Notez bien : ?? ne se déclenche que pour null et undefined, contrairement à || qui se déclencherait aussi pour 0, '' ou false. Pour un repli de valeur, ?? est presque toujours le bon choix.
2. L'impact sur les pipes
<!-- Un pipe reçoit undefined sans planter -->
<span>{{ user()?.name | translate }}</span>
<!-- Si user() est null, le pipe translate reçoit undefined.
La plupart des pipes (translate, uppercase…) gèrent undefined sans crash. -->
<!-- On peut sécuriser le repli AVANT le pipe -->
<span>{{ (user()?.name ?? 'Anonyme') | uppercase }}</span>
<!-- uppercase reçoit toujours un string : sortie 'ANONYME' garantie -->
?. + ?? couvre l'immense majorité des affichages : ?. traverse la chaîne sans planter, ?? fournit le repli quand un maillon manque. Ensemble, ils remplacent à la fois le $any() et les *ngIf="user" défensifs qui alourdissaient les templates d'avant.
strictTemplates : rendre les erreurs visibles
Tout ce qui précède repose sur une condition : que le compilateur de templates vérifie réellement vos expressions. C'est le rôle de strictTemplates, activé via les strict checks d'Angular (option strictTemplates dans tsconfig.json).
// tsconfig.json
{
"angularCompilerOptions": {
// Vérifie les types dans les templates aussi strictement que dans le TS
"strictTemplates": true
}
}
Sans strictTemplates, le compilateur est laxiste : il laisse passer des accès douteux et l'intérêt de ?. face à $any() s'efface, car les fautes ne sont de toute façon pas détectées. Avec lui, écrire user.name alors que user peut être null devient une erreur de compilation explicite — exactement le moment où l'on veut être alerté.
strictTemplates transforme les bugs de null et les fautes de frappe en erreurs de compilation. C'est précisément ce que $any() sabote en désactivant la vérification. Activer l'un et bannir l'autre, c'est la même démarche : déplacer les erreurs de la production vers l'éditeur.
Les nouveaux projets générés par ng new activent strictTemplates par défaut depuis plusieurs versions. Si votre projet est ancien, l'activer est l'une des améliorations de fiabilité les plus rentables que vous puissiez faire — et elle révélera probablement les $any() qui dormaient dans votre code.
Checklist : quand utiliser quoi
Voici la grille de décision à garder sous les yeux pendant vos revues de code. Pour chaque situation, l'outil le plus sûr.
?.— accéder à une propriété quand l'objet peut êtrenull/undefined?.()— appeler une méthode ou un callback qui peut ne pas exister?.[]— accès indexé sur un tableau/objet possiblement absent??— fournir une valeur par défaut (uniquement sur null/undefined)#ref— lire un élément du DOM (.value,.checked…) avec son vrai type- Type guard (narrowing) — valider une
stringcontre une union avant de l'accepter $any()— rarement : interop sans types, prototype jetable, faux positif documenté
Table de correspondance « problème → solution »
| Le problème | ❌ Le réflexe $any() | ✅ La bonne solution |
|---|---|---|
| Objet potentiellement null | $any(user).name |
user?.name |
| Lire la valeur d'un input | $any($event.target).value |
#q + q.value |
| Appeler une méthode incertaine | $any(obj).fn() |
obj?.fn?.() |
| Callback @Input optionnel | $any(cb)() |
cb?.() |
| Valeur à valider (union) | $any(s.value) |
type guard + set() |
| Valeur par défaut d'affichage | $any(...) || 'x' |
... ?? 'x' |
$any() est une question, pas une réponse. Demandez-vous « contre quoi me protège-t-il ? ». Si la réponse est « rien à l'exécution », c'est qu'un ?., un #ref ou un narrowing fera mieux.
Conclusion
$any() a la réputation d'un raccourci pratique, mais c'est un faux ami : il combine l'absence de sécurité de type (les fautes de frappe et les renommages passent inaperçus) et l'absence totale de protection à l'exécution (un null plante toujours). Il ne résout rien — il repousse le bug jusqu'en production, là où il coûte le plus cher.
Les outils du langage moderne couvrent chaque besoin réel : ?. pour traverser une chaîne null sans planter, ?.() et ?.[] pour les appels et les accès indexés incertains, ?? pour les valeurs par défaut, les template reference variables pour lire le DOM avec son vrai type, et le narrowing runtime pour valider les unions. Le tout sous l'œil de strictTemplates, qui transforme les erreurs en alertes de compilation.
Et n'oubliez pas la mise en garde du début : les assistants IA proposent encore trop souvent $any() comme première réponse à une erreur de typage. Relisez, remplacez, et faites de l'optional chaining votre réflexe par défaut. Un template Angular sûr n'est pas un template qui se tait — c'est un template qui ne ment pas.
$any()agit à la compilation, jamais à l'exécution?.conserve le typage et court-circuite àundefined- Préférez
#refau castas: vérifié plutôt que cru - Validez les unions par narrowing, pas par
$any() - Activez
strictTemplateset chassez les$any()résiduels