Angular 19 @let : variables locales dans les templates

🏷️ Front-end 📅 17/04/2026 18:00:00 👤 Mezgani said
Angular Angular 19 @Let Template Variables Directives Angular Angular 19 Nouveautés
Angular 19 @let : variables locales dans les templates

Découvrez @let dans Angular 19 : déclarez des variables locales dans vos templates pour simplifier l'async pipe, les blocs @if/@for et les Signals.

Le probleme avant @let

Avant Angular 19, les templates HTML ne disposaient d'aucun moyen natif pour stocker le resultat d'une expression dans une variable locale. Cette limitation obligeait les developpeurs a repeter les memes expressions complexes plusieurs fois dans le meme template, ce qui rendait le code verbeux et fragile.

Le cas le plus frequent : l'async pipe combine a une expression conditionnelle. Imaginons un observable qui retourne un objet utilisateur. Pour afficher le nom et l'email, il fallait deballer l'observable deux fois :

<!-- Template Angular AVANT @let -->
<!-- Probleme : user$ est deballe deux fois avec async pipe -->
<!-- Si l'observable emet une nouvelle valeur, les deux expressions -->
<!-- ne sont pas garanties d'etre evaluees au meme moment -->

<div *ngIf="(user$ | async) as user">
  <h1>{{ (user$ | async)?.name }}</h1>
  <p>{{ (user$ | async)?.email }}</p>
  <span>{{ (user$ | async)?.role }}</span>
</div>

Meme en utilisant la syntaxe as avec *ngIf, la portee de la variable etait limitee au bloc conditionnel. Impossible de reutiliser la variable en dehors du ngIf. Et avec la nouvelle syntaxe de flux de controle (@if, @for), la syntaxe as n'etait plus disponible de la meme facon.

A retenir : avant @let, deux patterns coexistaient : *ngIf="obs$ | async as val" (limitat la portee au bloc) et la repetition brute de l'expression. Les deux avaient des inconvenients serieux en maintenance et en lisibilite.

Autres situations problematiques sans @let :

  • Calculs repetes dans @for (prix total, concaténation de champs).
  • Expressions de template longues dupliquees dans plusieurs conditions.
  • Combinaison de plusieurs pipes sur la meme valeur.
  • Acces repete a des proprietes imbriquees profondes (ex: user.address.city.name).

Syntaxe et portee de @let

Introduit dans Angular 18 en developer preview et stabilise dans Angular 19, @let permet de declarer une variable locale directement dans le template. Sa syntaxe est simple et intentionnellement proche des variables JavaScript.

<!-- Syntaxe de base de @let -->
<!-- @let nomVariable = expression; -->

@let titre = 'Angular 19 @let';
@let total = prix * quantite;
@let nomComplet = prenom + ' ' + nom;
Note : le point-virgule en fin de declaration est obligatoire. Angular leve une erreur de compilation si vous l'oubliez.

Regles de portee (scope)

La portee d'une variable @let suit les memes regles que les blocs imbriques dans Angular. Une variable declaree dans un bloc @if, @for ou @switch n'est accessible que dans ce bloc et ses enfants. En dehors, elle est inconnue.

<!-- Portee globale dans le template -->
@let message = 'Bonjour depuis le template';

<!-- La variable message est accessible ici -->
<p>{{ message }}</p>

@if (isAdmin) {
  <!-- Variable locale au bloc @if -->
  @let labelAdmin = 'Espace administrateur';

  <!-- labelAdmin est accessible ici -->
  <h2>{{ labelAdmin }}</h2>
}

<!-- ERREUR : labelAdmin n'existe pas ici -->
<!-- <p>{{ labelAdmin }}</p> -->

Inference de type TypeScript

Angular infere automatiquement le type de la variable @let a partir de l'expression assignee. Pas besoin d'annoter le type manuellement : TypeScript analyse l'expression et applique le bon type dans les interpolations et les liaisons de propriete qui utilisent cette variable.

<!-- Inference de type : TypeScript deduit que 'total' est un number -->
@let total = items.reduce((sum, item) => sum + item.price, 0);

<!-- Inference de type : TypeScript deduit que 'label' est une string -->
@let label = user.firstName + ' (' + user.role + ')';

<!-- La variable beneficie du type inferee dans les bindings -->
<!-- angular signale une erreur si vous passez 'total' a une propriete string -->
<app-price-display [amount]="total"></app-price-display>
A retenir : @let est une variable en lecture seule. Elle est recalculee a chaque cycle de detection de changements. On ne peut pas lui assigner une nouvelle valeur depuis le template (pas de @let x = x + 1).

@let avec async pipe

Le cas d'usage le plus impactant de @let est la simplification des templates qui consomment des Observables via le pipe async. Fini le double abonnement, finie la variable imbriquee dans *ngIf : on deballe l'observable une seule fois et on reutilise la variable partout dans le template.

<!-- AVANT @let : l'observable etait deballe plusieurs fois -->
<div *ngIf="(user$ | async) as user">
  <!-- 'user' n'est accessible QUE dans ce bloc ngIf -->
  <h1>{{ user.name }}</h1>
  <p>{{ user.email }}</p>
</div>
<!-- Impossible d'utiliser 'user' ici sans un nouveau ngIf -->
<!-- APRES @let : l'observable est deballe UNE SEULE fois -->
<!-- La variable 'user' est disponible dans tout le template -->
@let user = user$ | async;

@if (user) {
  <!-- On reutilise 'user' sans re-deballer l'observable -->
  <h1>{{ user.name }}</h1>
  <p>{{ user.email }}</p>
  <span class="badge">{{ user.role }}</span>
}

<!-- Possible aussi en dehors du @if, avec une valeur potentiellement nulle -->
<p aria-live="polite">
  Connecte en tant que : {{ user?.name ?? 'Chargement...' }}
</p>

Exemple complet : profil utilisateur avec etat de chargement

<!-- Composant UserProfileComponent -->
<!-- user$ : Observable<User | null> -->
<!-- isLoading$ : Observable<boolean> -->

<!-- On declare les deux variables une seule fois -->
@let user = user$ | async;
@let loading = isLoading$ | async;

@if (loading) {
  <!-- Affichage du spinner pendant le chargement -->
  <div class="text-center py-4" role="status" aria-label="Chargement du profil">
    <div class="spinner-border text-primary"></div>
  </div>
} @else if (user) {
  <!-- Profil complet : on reutilise 'user' sans abonnement supplementaire -->
  <div class="card shadow-sm">
    <div class="card-body">
      <h2 class="card-title">{{ user.name }}</h2>
      <p class="card-text">{{ user.email }}</p>
      <span class="badge badge-primary">{{ user.role }}</span>
      <!-- On passe 'user' directement a un composant enfant -->
      <app-user-actions [user]="user"></app-user-actions>
    </div>
  </div>
} @else {
  <p class="text-muted">Aucun utilisateur connecte.</p>
}
Note : quand l'observable n'a pas encore emis de valeur, user$ | async retourne null. La variable @let user aura donc le type User | null. Pensez a le gerer avec @if (user) ou l'operateur ?..

@let avec @if, @for et @switch

La combinaison de @let avec les nouveaux blocs de flux de controle d'Angular est naturelle et tres lisible. Chaque bloc peut avoir ses propres variables locales, ce qui evite de surcharger le composant TypeScript avec des proprietes de pure presentation.

@let dans un bloc @for

<!-- Liste de produits avec calcul du prix TTC local -->
<!-- 'products' est un tableau de Product[] dans le composant -->

@for (product of products; track product.id) {
  <!-- Variable locale par iteration : evite de repeter le calcul -->
  @let priceTTC = product.price * 1.20;

  <!-- 'isPromo' est aussi une variable locale calculee -->
  @let isPromo = product.stock < 10 && product.price > 50;

  <div class="card mb-3" [class.border-warning]="isPromo">
    <div class="card-body">
      <h3 class="card-title">{{ product.name }}</h3>
      <!-- On reutilise priceTTC sans recalculer l'expression -->
      <p class="card-text">Prix TTC : {{ priceTTC | currency:'EUR' }}</p>
      @if (isPromo) {
        <!-- isPromo est accessible dans les blocs enfants -->
        <span class="badge badge-warning">Promo stock limite</span>
      }
    </div>
  </div>
}

@let dans un bloc @switch

<!-- Affichage conditionnel selon le statut d'une commande -->
@let statusLabel = order.status === 'pending'   ? 'En attente'
                 : order.status === 'shipped'   ? 'Expediee'
                 : order.status === 'delivered' ? 'Livree'
                 : 'Statut inconnu';

<!-- On utilise statusLabel dans le template sans repeter la logique -->
<span class="order-status">{{ statusLabel }}</span>

@switch (order.status) {
  @case ('pending') {
    <!-- Variable locale au case -->
    @let eta = order.estimatedDays + ' jours';
    <p>Livraison estimee : {{ eta }}</p>
  }
  @case ('shipped') {
    @let trackingUrl = 'https://tracking.example.com/' + order.trackingCode;
    <a [href]="trackingUrl" target="_blank" rel="noopener">
      Suivre le colis
    </a>
  }
  @default {
    <p class="text-muted">Aucune information disponible.</p>
  }
}

@let pour simplifier les expressions imbriquees

<!-- Sans @let : expression profonde repetee 3 fois -->
<p>{{ user.profile.address.city }}</p>
<p>{{ user.profile.address.city }}, {{ user.profile.address.country }}</p>
<small>Code postal : {{ user.profile.address.city }} - {{ user.profile.address.zip }}</small>

<!-- Avec @let : on cree un alias court, plus lisible -->
@let addr = user.profile.address;
<p>{{ addr.city }}</p>
<p>{{ addr.city }}, {{ addr.country }}</p>
<small>Code postal : {{ addr.city }} - {{ addr.zip }}</small>
A retenir : @let ne remplace pas la logique metier dans le composant TypeScript. Si une expression est complexe ou reutilisee au-dela du template, deplacez-la dans une propriete computed() ou une methode du composant.

@let avec les Signals Angular

Les Signals sont la nouvelle primitive reactives d'Angular. Dans un template, lire un signal se fait en l'appelant comme une fonction : monSignal(). Quand on accede plusieurs fois au meme signal dans le template, chaque appel est une lecture independante. @let permet de lire le signal une seule fois et de reutiliser sa valeur.

<!-- Composant TypeScript -->
import { Component, signal, computed } from '@angular/core';

@Component({
  selector: 'app-panier',
  templateUrl: './panier.component.html'
})
export class PanierComponent {
  // Signal contenant la liste des articles du panier
  readonly items = signal<{ name: string; price: number; qty: number }[]>([]);

  // Signal derive : total calcule automatiquement quand items change
  readonly total = computed(() =>
    this.items().reduce((sum, item) => sum + item.price * item.qty, 0)
  );

  // Signal d'etat de chargement
  readonly isLoading = signal<boolean>(false);
}
<!-- Template : on lit chaque signal une seule fois avec @let -->
@let cartItems = items();
@let cartTotal = total();
@let loading = isLoading();

@if (loading) {
  <!-- Affichage pendant le chargement initial -->
  <p role="status">Chargement du panier...</p>
} @else {
  <!-- Liste des articles -->
  @for (item of cartItems; track item.name) {
    <!-- Variable locale : sous-total par article -->
    @let lineTotal = item.price * item.qty;

    <div class="row align-items-center mb-2">
      <div class="col">{{ item.name }}</div>
      <div class="col-auto">{{ item.qty }} x {{ item.price | currency:'EUR' }}</div>
      <!-- On reutilise lineTotal sans recalculer -->
      <div class="col-auto fw-bold">{{ lineTotal | currency:'EUR' }}</div>
    </div>
  }

  <!-- Total general, lu une seule fois -->
  <div class="border-top pt-3 fw-bold">
    Total : {{ cartTotal | currency:'EUR' }}
  </div>
}

@let avec un Signal expose via computed()

Un pattern frequent : le composant expose un computed() complexe, et le template le lit via @let pour l'utiliser dans plusieurs endroits sans multiplier les appels de la fonction signal.

<!-- Composant TypeScript -->
@Component({ /* ... */ })
export class DashboardComponent {
  // Signal contenant les statistiques brutes
  readonly stats = signal<{ visitors: number; sales: number; returns: number }>({
    visitors: 0,
    sales: 0,
    returns: 0
  });

  // Signal derive : taux de conversion calcule
  readonly conversionRate = computed(() => {
    const s = this.stats();
    // Evite une division par zero
    return s.visitors > 0
      ? ((s.sales / s.visitors) * 100).toFixed(1)
      : '0.0';
  });
}
<!-- Template : on lit le signal computed une seule fois -->
@let rate = conversionRate();
@let s = stats();

<div class="row">
  <div class="col-md-4">
    <div class="card text-center">
      <div class="card-body">
        <h3 class="card-title">{{ s.visitors }}</h3>
        <p class="card-text">Visiteurs</p>
      </div>
    </div>
  </div>
  <div class="col-md-4">
    <div class="card text-center">
      <div class="card-body">
        <h3 class="card-title">{{ s.sales }}</h3>
        <p class="card-text">Ventes</p>
      </div>
    </div>
  </div>
  <div class="col-md-4">
    <!-- On affiche rate a deux endroits sans rappeler conversionRate() -->
    <div class="card text-center">
      <div class="card-body">
        <h3 class="card-title">{{ rate }}%</h3>
        <p class="card-text">Taux de conversion</p>
      </div>
    </div>
  </div>
</div>

<!-- On reutilise rate pour afficher un message contextuel -->
@if (+rate > 5) {
  <div class="alert alert-success">Excellent taux de conversion : {{ rate }}%</div>
} @else {
  <div class="alert alert-warning">Taux a ameliorer : {{ rate }}%</div>
}
Note : lire un signal plusieurs fois dans un template n'est pas "dangereux" (Angular optimise la detection de changements), mais utiliser @let rend le template plus lisible et evite des incoherences visuelles si le signal emettait une valeur differente entre deux lectures dans le meme cycle.

@let vs template reference variables

Angular dispose depuis longtemps des template reference variables (prefixees par #). Ces deux mecanismes ne font pas la meme chose et ne se remplacent pas. Voici les differences cles :

Critere @let Template reference (#ref)
Declaration @let val = expression; #refName sur un element
Contenu Resultat d'une expression (valeur) Reference a un element DOM ou composant
Portee Bloc courant et enfants Tout le template (meme hors bloc)
Modifiable Non (lecture seule) Non (pointe vers l'instance)
Acces TypeScript Pas d'acces depuis le composant Via @ViewChild / @ContentChild
Cas d'usage principal Alias de valeur, eviter repetition Manipuler un element DOM ou appeler une methode de composant enfant
<!-- Template reference variable : acces a un element DOM natif -->
<input #emailInput type="email" class="form-control" />

<!-- On appelle une methode sur l'element via la reference -->
<button (click)="emailInput.focus()">Focus email</button>

<!-- @let : calcul d'une valeur a partir de la reference -->
@let inputLength = emailInput.value.length;
<small>{{ inputLength }} caracteres</small>
<!-- Template reference sur un composant enfant -->
<!-- Permet d'appeler des methodes publiques du composant -->
<app-modal #confirmModal></app-modal>

<!-- @let n'est PAS adapte pour ca : il ne donne pas acces aux methodes -->
<button (click)="confirmModal.open()">Ouvrir la modale</button>
Regle simple : utilisez #ref quand vous avez besoin d'appeler une methode ou d'acceder a un element DOM. Utilisez @let quand vous voulez stocker le resultat d'un calcul ou simplifier une expression.

Limites et pieges a eviter

Comme toute nouveaute, @let a ses limites. Les connaitre evite les mauvaises surprises en production.

1. @let est en lecture seule

On ne peut pas reassigner une variable @let dans le template. Toute tentative de modification provoque une erreur de compilation.

<!-- ERREUR : reassignation impossible -->
@let count = 0;
<button (click)="count = count + 1">Incrementer</button>
<!-- Angular leve une erreur : 'count' is a @let variable and cannot be assigned -->

<!-- CORRECT : l'etat mutable doit etre dans le composant -->
<!-- Dans le composant TypeScript : count = signal(0); -->
@let countValue = count();
<button (click)="count.set(count() + 1)">Incrementer</button>
<p>{{ countValue }}</p>

2. @let ne persiste pas entre les cycles

La variable @let est recalculee a chaque cycle de detection de changements. Elle ne memorise aucune valeur precedente. C'est une limitation intentionnelle : @let est un alias d'expression, pas un etat.

<!-- Mauvais usage : tenter de "memoiser" un calcul lourd -->
<!-- Ce calcul est RE-execute a CHAQUE cycle de change detection -->
@let result = computeExpensiveOperation(data);

<!-- CORRECT : si le calcul est lourd, utilisez computed() dans le composant -->
<!-- Dans le composant : readonly result = computed(() => computeExpensiveOperation(this.data())); -->
@let result = result();

3. @let n'est pas accessible depuis le TypeScript

Contrairement aux template reference variables accessibles via @ViewChild, une variable @let n'est visible que dans le template. Elle n'existe pas dans la classe du composant.

<!-- Template -->
@let discount = price * 0.15;

<!-- IMPOSSIBLE dans le composant TypeScript -->
<!-- this.discount --> // undefined : @let n'existe que dans le template

4. @let ne fonctionne pas avant la declaration

Comme les variables const en JavaScript (temporal dead zone), une variable @let ne peut pas etre utilisee avant sa declaration dans le template.

<!-- ERREUR : utilisation avant declaration -->
<p>{{ fullName }}</p>
@let fullName = firstName + ' ' + lastName;

<!-- CORRECT : declaration avant utilisation -->
@let fullName = firstName + ' ' + lastName;
<p>{{ fullName }}</p>

5. Eviter la sur-utilisation dans le template

Si vous vous retrouvez a declarer plus de 5 ou 6 @let dans un template, c'est souvent le signe que la logique devrait etre dans le composant TypeScript (via des proprietes computed() ou des methodes). @let est fait pour simplifier, pas pour remplacer le composant.

Bonne pratique : reservez @let aux alias courts qui ameliorent la lisibilite du template. Pour la logique metier, les calculs lourds ou les valeurs reutilisees dans le composant TypeScript, privilegiez les proprietes computed() ou les getters.

Recapitulatif des pieges

  • Ne pas tenter de reassigner une variable @let depuis un evenement.
  • Ne pas utiliser @let pour memoiser un calcul couteux (utiliser computed()).
  • Ne pas declarer @let apres son premier usage dans le template.
  • Ne pas acceder a la variable @let depuis le composant TypeScript.
  • Ne pas surcharger le template avec trop de @let : deplacer la logique dans le composant.

Conclusion

@let est une addition discrete mais tres utile dans la boite a outils Angular 19. Elle resout un probleme reel : l'absence de variable locale dans les templates, qui forcait les developpeurs a repeter des expressions complexes ou a contourner la limitation avec des patterns verbeux comme *ngIf="obs$ | async as val".

En combinant @let avec les nouveaux blocs de flux de controle (@if, @for, @switch) et avec les Signals, vous obtenez des templates plus courts, plus lisibles et plus maintenables, sans deplacer de logique presentationnelle vers le composant TypeScript. Utilisez-le avec discernement : pour les alias courts et les expressions repetees, pas pour remplacer la logique metier.

A retenir : @let est un alias d'expression en lecture seule, scoped au bloc courant, recalcule a chaque cycle de detection de changements. Il simplifie les templates sans remplacer computed() ni les proprietes du composant.