~12 min de lecture

Ton agent IA écrit du mauvais Angular ? C'est ta codebase qui le lui apprend : 7 conventions à poser

TL;DR

Un agent de code ne lit pas ton repo comme toi : il l'échantillonne au grep, lit une poignée de fichiers, et imite le pattern majoritaire qu'il y trouve. Codebase à moitié migrée, mal nommée, laxiste sur le typage : l'agent produit exactement ça. Les 7 conventions de cet article ne sont pas des conventions "pour l'IA" : ce sont celles qui rendent une codebase lisible tout court. L'agent est juste le révélateur le plus brutal de leur absence.

Tu as branché Claude Code, Cursor ou Copilot sur ton projet Angular. Tu demandes "ajoute un filtre par statut sur la liste des factures". L'agent te rend un composant avec @Input(), un store NgRx pour un état local à la page, un fichier posé dans le mauvais dossier et un any au milieu. Tu conclus que "l'IA code mal en Angular".

Mauvais diagnostic. Le modèle connaît input(), les signals et le control flow moderne : il les utilise très bien sur un repo propre. Un agent de code est une machine à imiter ta codebase, et la tienne lui a appris ça.

Et contrairement au dev qui débarque sur le projet, l'agent ne vient pas te demander "c'est quoi la convention ici ?" : il tranche en silence, des dizaines de fois par session.

Comment un agent lit ton repo (spoiler : pas en entier)

Avant les conventions, le mécanisme. Un agent ne charge jamais ton repo entier : sa fenêtre de contexte (la quantité de texte qu'il peut avoir sous les yeux à un instant donné) est limitée, et ton src/ la dépasse souvent. À la place, il fait ce que tu fais dans un projet inconnu, en accéléré :

  1. il cherche par nom de fichier (glob, un motif comme **/*invoice*) et par contenu (grep) ;
  2. il lit la poignée de fichiers qui ressemblent le plus à sa tâche ;
  3. il imite ce qu'il y voit, pattern majoritaire en tête ;
  4. il vérifie avec ce que tu lui donnes : compilateur, tests, lint.

Chaque étape est un levier : le nommage décide de ce que l'étape 1 trouve, la cohérence de ce que l'étape 3 reproduit, le typage et les tests de ce que l'étape 4 attrape. Les 7 conventions qui suivent agissent chacune sur l'un de ces leviers, de la plus rentable à la moins rentable.

1. Le pattern majoritaire gagne : finis tes migrations

C'est la convention la plus rentable et la moins appliquée. Une migration à moitié faite ne protège de rien : un repo 100 % @Input() est au moins cohérent, un repo 60/40 apprend à l'agent que les deux styles sont acceptables, et c'est le style majoritaire qui l'emporte.

Angular fournit des schematics de migration, des scripts qui réécrivent ton code et se lancent avec ng generate. Une commande par pattern :

# control flow @if/@for (Angular 17+)
ng generate @angular/core:control-flow

# constructeur vers inject() (Angular 18.2+)
ng generate @angular/core:inject

# @Input() vers input() (Angular 19+)
ng generate @angular/core:signal-input-migration

# @Output() vers output() (Angular 19+)
ng generate @angular/core:output-migration

Aucune ne promet le 100 % : un input que le composant réécrit ou un output sur lequel tu chaînes des opérateurs (.pipe(...)) restent sur le carreau. Relance signal-input-migration avec --insert-todos : elle écrit ses raisons en commentaire. output-migration n'a pas cette option et n'affiche qu'un décompte : cherche les @Output() restants au grep. Migre ces cas à la main, sinon tu recrées le 60/40. Les angles morts sont détaillés dans les migrations que tu n'as jamais lancées.

Avant, ce que l'agent trouve en majorité et reproduit :

import { Component, EventEmitter, Input, Output } from '@angular/core';
import { Invoice } from '../models/invoice';

@Component({
  selector: 'app-invoice-table',
  templateUrl: './invoice-table.html',
})
export class InvoiceTable {
  @Input({ required: true }) invoices!: Invoice[];
  @Input() currency = 'EUR';
  @Output() selected = new EventEmitter<Invoice>();
}

Après migration, ce qu'il trouve et reproduit à la place :

import { ChangeDetectionStrategy, Component, input, output } from '@angular/core';
import { Invoice } from '../models/invoice';

@Component({
  selector: 'app-invoice-table',
  templateUrl: './invoice-table.html',
  changeDetection: ChangeDetectionStrategy.OnPush,
})
export class InvoiceTable {
  readonly invoices = input.required<Invoice[]>();
  readonly currency = input('EUR');
  readonly selected = output<Invoice>();
}

Le OnPush explicite ne vient d'aucune de ces migrations : jusqu'en Angular 21, c'est une convention à poser toi-même. Depuis la v22, OnPush est le défaut, et la montée de version pose ChangeDetectionStrategy.Eager sur les composants sans stratégie : encore un pattern hérité que l'agent recopiera si tu ne le résorbes pas.

Le mélange des époques ne coûte pas qu'en style, il produit des bugs silencieux. Prends un *ngIf aperçu dans un vieux composant déclaré dans un NgModule, où CommonModule le fournissait sans import visible. Un agent qui le recopie dans un composant standalone sans importer NgIf rend un code qui compile et n'affiche rien : la directive n'est pas appliquée. Le build ne s'en plaint (avertissement NG8103) que si strictTemplates est actif (convention 3) ; à l'exécution, il reste par défaut une erreur NG0303 dans la console en développement, et rien du tout en production. Exactement le genre de câblage qu'un repo à deux styles fait rater.

Si tu ne peux vraiment pas tout migrer (zone legacy gelée, lib interne), isole le legacy dans un dossier clairement nommé et dis-le par écrit (convention 5) : "tout ce qui est sous legacy/ est en cours de sortie, n'imite rien de ce qui s'y trouve". Un agent à qui on ne dit rien imite la majorité, legacy compris.

2. Nomme tes fichiers comme on les cherche

L'étape 1 de l'agent, c'est de la recherche par nom. "Ajoute un filtre sur la liste des factures" déclenche un glob sur *invoice*. Si ton composant s'appelle invoice-table.ts, il est trouvé au premier essai. S'il s'appelle table.ts dans un dossier billing/components/shared/, l'agent le trouvera peut-être, trois recherches et deux mauvaises pistes plus tard, chaque détour brûlant du contexte qui manquera pour la vraie tâche.

Le style guide Angular (révisé en v20) pousse déjà dans le bon sens : plus de suffixe .component.ts imposé, des noms de classes sans suffixe Component (InvoiceTable, pas InvoiceTableComponent). Mais la règle qui compte pour un agent n'est pas le style choisi, c'est son uniformité : un repo où invoice-table.ts, user-card.component.ts et TableFactures.ts cohabitent est illisible au glob, quel que soit le style gagnant.

Les fichiers fourre-tout privent l'agent de la recherche par nom. Un nom comme utils.ts ou helpers.ts ne dit rien de ce que le fichier contient, et aucun glob sur la feature n'y mène. Il ne reste que le grep, à l'aveugle : sur le nom de la fonction, qu'il faut deviner (formatDate ? toDisplayDate ? dateLabel ?), ou sur son implémentation, qu'il faut deviner aussi. Faute de la trouver, l'agent réécrit la sienne à côté, et la duplication que tu mettras sur le dos de l'IA aura pour vrai coupable le fichier fourre-tout.

3. Le typage strict est la boucle de feedback de l'agent

Un agent itère contre ton compilateur : il écrit, lance tsc ou le build, lit les erreurs, corrige. Chaque erreur de type est un garde-fou qui rattrape une hallucination avant toi. Un any est un trou dans ce filet : le code faux compile, et c'est toi qui le découvres en review.

Concrètement :

  • "strict": true dans tsconfig.json (défaut des projets générés depuis Angular 12, mais vérifie les vieux repos migrés) ;
  • "noUncheckedIndexedAccess": true : l'accès par index sur un tableau ou un dictionnaire rend T | undefined, l'agent doit traiter le cas absent au lieu de le supposer ;
  • "strictTemplates": true dans angularCompilerOptions : la moitié template du filet, celle qui type les bindings et $event. Sans elle, l'agent peut passer un number à un input qui attend un objet, et aucun outil ne proteste ;
  • l'interdiction de any et des assertions de type (as) au lint (no-explicit-any, plus consistent-type-assertions réglée sur assertionStyle: 'never', sans quoi elle laisse passer les as) : un agent pressé par une erreur de type a le même réflexe qu'un dev pressé, caster pour faire taire, et no-explicit-any seul laisse passer un as Invoice ;
  • des frontières typées : ce qui entre par HTTP est validé (Zod ou équivalent) plutôt que casté, sinon tes types internes mentent et l'agent raisonne sur un mensonge.

Un piège sur strictTemplates à la montée en v22 : Angular 22 l'active par défaut, et un ng new standard de la v12 à la v21 pose l'option à la racine et n'est pas concerné. Mais si ton projet ne l'avait nulle part (créé avant Angular 12 sans --strict, ou avec --strict=false), la migration de ng update écrit strictTemplates: false dans les tsconfig que tes builds et tes tests déclarent (tsconfig.app.json, tsconfig.spec.json). C'est dans ces fichiers qu'il faut remettre true : posé seulement dans le tsconfig.json racine, il serait écrasé par le false des fichiers qui l'étendent.

4. Des tests rapides, sinon l'agent ne se vérifie pas

Même logique que le typage, un cran plus haut : les tests sont le deuxième vérificateur de l'agent. À une condition que personne ne formule : la boucle doit être courte. Une suite Vitest qui rend son verdict en quelques secondes sera relancée après chaque modification. Une suite qui met plusieurs minutes à démarrer sera lancée une fois à la fin, voire pas du tout, et l'agent te livrera du code "qui devrait marcher".

Ce que ça implique :

  • une commande unique et évidente (pnpm test), qui marche sans setup manuel ;
  • une façon documentée de cibler un fichier (avec ng test, c'est --include ; avec Vitest en direct, le chemin suffit), pour que l'agent ne paie que pour le périmètre qu'il touche ;
  • des tests qui décrivent le comportement, pas l'implémentation : un test qui casse dès qu'on renomme une méthode privée apprend à l'agent (comme au junior) que "casser des tests, c'est normal", et l'agent prendra l'habitude de les adapter au lieu de s'interroger.

Les tests jouent un double rôle : vérificateur, mais aussi spécification. Des tests lisibles sont de la documentation que ta CI empêche de mentir.

5. Écris les décisions, pas la doc

Un fichier d'instructions à la racine (CLAUDE.md, AGENTS.md selon l'outil) est lu au début de chaque session : quelques centaines de lignes qui conditionnent tout ce que l'agent produit. La plupart de ces fichiers sont mal remplis, parce qu'on y met de la doc au lieu d'y mettre des décisions.

La différence :

<!-- Doc : l'agent le voit dans le code, tu gaspilles sa lecture -->
Le projet utilise Angular et TailwindCSS. Les composants sont dans src/app.

<!-- Décision : invisible dans le code, c'est ça qu'il faut écrire -->
- Les pages sont en `export default` (requis par `loadComponent()`),
  tout le reste en export nommé. Ne jamais inverser.
- Sur les données serveur, `track` sur un identifiant métier
  (`track item.id`), jamais `track $index` ni `track item`.
- On ne crée pas de store NgRx pour un état local à une page :
  un signal dans le composant suffit. Un store se justifie pour
  un état lu ET écrit depuis plusieurs endroits sans lien
  parent-enfant ; une lecture partagée se règle en service.

Une règle non écrite n'existe pas pour un agent. Les conventions "orales" de ton équipe sont exactement ce qui doit figurer dans ce fichier : le choix, la raison en une phrase, la commande qui vérifie quand elle existe. Si tu ne sais pas quoi y mettre, ouvre tes 20 dernières reviews et note ce que tu as répété deux fois.

6. Mécanise ce que tu as écrit

Une règle écrite dans CLAUDE.md, c'est bien. La même règle en lint, c'est mieux, pour une question de timing : l'instruction écrite agit en début de session et se dilue à mesure que le contexte se remplit ; la règle mécanisée frappe au moment exact de la violation. Un agent obéit mieux à un code de sortie non nul (la commande échoue, la CI vire au rouge) qu'à un paragraphe.

Pour les patterns visés par la convention 1, inutile de coder une règle maison : angular-eslint fournit les règles toutes faites. prefer-signals rejette @Input() et les queries par décorateur (@ViewChild() et compagnie), prefer-output-emitter-ref rejette @Output(), prefer-inject l'injection par constructeur, template/prefer-control-flow les *ngIf / *ngFor, et prefer-on-push-component-change-detection les ChangeDetectionStrategy.Eager résiduels de la migration v22. Réglées en error et branchées en CI, elles arrêtent l'agent avant que tu voies le diff, et elles empêchent le repo de se remplir de nouveau de mauvais exemples.

Dans un monorepo Nx, la règle la plus rentable à mécaniser est la frontière entre libs. Chaque lib porte des tags dans son project.json (scope: pour le domaine métier, type: pour la couche), et la règle dit quel tag a le droit de dépendre de quel autre :

// eslint.config.mjs
rules: {
  '@nx/enforce-module-boundaries': ['error', {
    depConstraints: [
      {
        sourceTag: 'scope:billing',
        onlyDependOnLibsWithTags: ['scope:billing', 'scope:shared'],
      },
      {
        sourceTag: 'type:ui',
        onlyDependOnLibsWithTags: ['type:ui', 'type:util'],
      },
    ],
  }],
},

Sans cette règle, un agent qui a besoin d'un service le trouve au grep et l'importe, peu importe la couche. Avec elle, il reçoit une erreur explicite qui lui interdit la dépendance, et doit passer par une lib autorisée (scope:shared, par exemple). Même logique pour tout invariant maison : un script qui échoue en CI avec un message clair protège une règle mieux qu'une relecture.

7. Des unités petites aux dépendances visibles

Dernière convention, côté structure. Un composant de 800 lignes pose deux problèmes à un agent : il dépasse ce que l'agent lit attentivement (le début est lu, le reste survolé, et l'agent modifie un code qu'il n'a pas vraiment lu), et il mélange des responsabilités qu'il faut démêler à chaque tâche.

Les composants standalone, par défaut depuis Angular 19, aident plus qu'on ne le dit : le tableau imports du composant rend ses dépendances de template visibles dans le fichier même. Avec les NgModules, savoir d'où venait une directive demandait de remonter les modules ; l'agent qui ne fait pas cette remontée invente des imports ou en oublie.

Le découpage smart/dumb (un composant conteneur qui injecte les services et tient l'état, des composants de présentation qui ne font qu'afficher) joue dans le même sens : un composant de présentation qui ne reçoit que des input() et n'émet que des output() se modifie sans comprendre le reste de l'app. C'est exactement le périmètre qu'un agent traite bien.

Récap actionnable

Dans l'ordre du retour sur investissement :

  1. Termine tes migrations : les schematics font l'essentiel ; ce qu'elles refusent, tu le finis à la main (--insert-todos explique les refus de signal-input-migration). Le pattern majoritaire est ce que l'agent reproduit.
  2. Un style de nommage unique, prévisible au glob, et pas de utils.ts fourre-tout : le nom de la feature permet de deviner le nom du fichier.
  3. Typage strict : strict, noUncheckedIndexedAccess, strictTemplates, zéro any ni assertion de type (as), frontières HTTP validées. Une erreur de type, c'est une hallucination interceptée.
  4. Tests en secondes, pas en minutes, ciblables par fichier, orientés comportement.
  5. Un fichier d'instructions qui liste des décisions (le choix, la raison, la commande qui vérifie), pas une visite guidée du repo.
  6. Mécanise les règles qui comptent : règles angular-eslint toutes faites (prefer-signals, prefer-inject), frontières entre libs Nx (enforce-module-boundaries), lint custom, scripts CI. Un code de sortie bat un paragraphe.
  7. Des unités petites aux dépendances visibles : standalone, smart/dumb, pas de fichier de 800 lignes.

Aucune de ces conventions n'est spécifique à l'IA : c'est la même liste qu'on donnerait pour onboarder un dev. La différence, c'est le volume. Un humain nouveau sur le projet prend ses décisions au compte-gouttes et vient te voir quand il doute ; un agent en prend en continu et ne doute jamais à voix haute. Si ton agent code mal chez toi, c'est peut-être le meilleur audit gratuit que ta codebase ait jamais reçu. Pose ces conventions une fois, et tout ce qui lit ton repo, agents comme humains, produira du code qui te ressemble.

📧 Reste informé(e) !

Reçois les derniers articles et conseils EasyAngularKit directement dans ta boîte mail.

S'inscrire gratuitement

AngularKit

Suite d'outils pour développeurs Angular francophones. Apprends, modernise tes réflexes, audite ta codebase.

Produits

Contact

Légal

© 2026 AngularKit. Tous droits réservés.