Activez le strict mode TypeScript et configurez tsconfig.json pour Angular : strictNullChecks, noUncheckedIndexedAccess, migration progressive, alias de chemins et tsconfig par environnement.
Pourquoi activer le strict mode TypeScript
Le strict mode TypeScript n'est pas une contrainte arbitraire : c'est un investissement mesurable en qualite de code. Activer "strict": true dans votre tsconfig.json active un ensemble de verifications supplementaires qui eliminent des categories entieres de bugs avant meme l'execution.
Voici les benefices concrets et mesurables observes en production :
- Bugs elimines a la compilation : les acces a
nullouundefinednon verifies sont detectes avant le runtime. - Refactorisations plus sures : le compilateur signale toutes les zones impactees par un changement de signature ou de type.
- Code plus expressif : les types deviennent une documentation fiable et toujours a jour.
- Onboarding accelere : un nouveau developpeur comprend les intentions du code via les types, sans lire la logique.
- Tests reduits : certains tests unitaires defensifs deviennent inutiles quand le compilateur garantit les invariants.
"strict": true par defaut depuis Angular 12 pour tous les nouveaux projets generes avec ng new. Si votre projet est plus ancien, il est probablement en mode non-strict.
// Verification rapide : votre projet est-il en strict mode ?
// Ouvrez tsconfig.json et cherchez la cle "strict"
// Mode NON strict (valeur par defaut historique)
{
"compilerOptions": {
"strict": false // ou absente
}
}
// Mode strict active
{
"compilerOptions": {
"strict": true // active 7 verifications en une seule option
}
}
Les options du flag strict expliquees une par une
Le flag "strict": true est en realite un raccourci qui active simultanement 7 options independantes. Les connaitre individuellement permet de les activer progressivement sur un projet existant.
strictNullChecks
C'est l'option la plus impactante. Sans elle, null et undefined sont assignables a n'importe quel type, ce qui masque des erreurs frequentes en runtime.
// Sans strictNullChecks : aucune erreur a la compilation
function getLength(s: string): number {
return s.length; // Crash si s est null en runtime
}
getLength(null); // Accepte sans avertissement
// Avec strictNullChecks : erreur claire a la compilation
function getLength(s: string): number {
return s.length;
}
getLength(null); // Erreur : Argument of type 'null' is not assignable to parameter of type 'string'
// Correction : typage explicite du cas nullable
function getLength(s: string | null): number {
if (s === null) return 0; // Gestion explicite du cas null
return s.length;
}
strictFunctionTypes
Active la verification contravariante des types de parametres de fonctions. Evite des erreurs de sous-typage de callbacks qui causent des bugs silencieux.
// Sans strictFunctionTypes : assignation acceptee mais dangereuse
type Handler = (event: MouseEvent) => void;
// MouseEvent est un sous-type de Event
// Assigner un handler Event a un Handler MouseEvent est incorrect
const handler: Handler = (e: Event) => console.log(e); // Accepte sans strict
// Avec strictFunctionTypes : erreur detectee
// Type '(e: Event) => void' is not assignable to type '(event: MouseEvent) => void'
// Les parametres sont contravarients : Event est plus general que MouseEvent
// Correction : utiliser le bon type de parametre
const handler: Handler = (e: MouseEvent) => console.log(e.clientX);
strictPropertyInitialization
Verifie que toutes les proprietes de classe declarees sont initialisees dans le constructeur ou avec une valeur par defaut.
// Sans strictPropertyInitialization : propriete potentiellement undefined
class UserComponent {
name: string; // Jamais initialisee = undefined en runtime
greet(): string {
return `Bonjour ${this.name.toUpperCase()}`; // Crash si name est undefined
}
}
// Avec strictPropertyInitialization : erreur a la compilation
// Property 'name' has no initializer and is not definitely assigned
// Solution 1 : valeur par defaut
class UserComponent {
name: string = ''; // Initialisee avec une chaine vide
}
// Solution 2 : initialisation dans le constructeur
class UserComponent {
name: string;
constructor(name: string) {
this.name = name; // Initialisee dans le constructeur
}
}
// Solution 3 : type nullable avec verifications
class UserComponent {
name: string | null = null; // Declare explicitement nullable
}
noImplicitAny
Interdit les types any implicites. Chaque variable ou parametre sans type doit avoir un type inferable ou explicite.
// Sans noImplicitAny : TypeScript infere 'any' silencieusement
function process(data) { // data a le type implicite 'any'
return data.value; // Aucune verification, peut crasher
}
// Avec noImplicitAny : erreur
// Parameter 'data' implicitly has an 'any' type
// Correction : typer explicitement les parametres
interface DataObject {
value: string;
timestamp: number;
}
function process(data: DataObject): string {
return data.value; // TypeScript verifie que .value existe et est une string
}
noImplicitThis
Signale les utilisations de this dont le type est implicitement any, notamment dans les fonctions classiques (non-arrow).
// Sans noImplicitThis : 'this' a le type 'any', acces non verifie
function getTitle() {
return this.title; // 'this' est implicitement 'any'
}
// Avec noImplicitThis : erreur
// 'this' implicitly has type 'any' because it does not have a type annotation
// Correction : declarer le type de 'this' comme premier parametre fictif
interface Article {
title: string;
}
function getTitle(this: Article): string {
return this.title; // TypeScript sait que 'this' est de type Article
}
alwaysStrict
Injecte 'use strict' dans chaque fichier JavaScript emis. Active le mode strict ECMAScript dans le runtime, ce qui interdit notamment les variables non declarees.
// Avec alwaysStrict : TypeScript injecte automatiquement "use strict"
// dans chaque fichier .js genere par la compilation
// Le fichier TypeScript :
export function calculate(a: number, b: number): number {
return a + b;
}
// Devient en JavaScript compile :
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.calculate = calculate;
function calculate(a, b) {
return a + b;
// "use strict" empeche les erreurs silencieuses de JS classique
}
strictBindCallApply
Verifie que les arguments passes a .bind(), .call() et .apply() correspondent aux types de la fonction originale.
// Sans strictBindCallApply : arguments non verifies
function add(a: number, b: number): number {
return a + b;
}
add.call(null, 1, 'deux'); // Accepte sans erreur malgre le type incorrect
// Avec strictBindCallApply : erreur a la compilation
// Argument of type 'string' is not assignable to parameter of type 'number'
add.call(null, 1, 2); // Correct : les types correspondent
// Meme verification pour .bind() et .apply()
const boundAdd = add.bind(null, 1); // boundAdd est type (b: number) => number
boundAdd('deux'); // Erreur detectee par strictBindCallApply
"strict": true est present mais qu'une option pose probleme, vous pouvez la desactiver explicitement : "strictPropertyInitialization": false.
Options avancees recommandees
Ces trois options ne font pas partie du flag "strict" mais sont fortement recommandees pour une securite de types maximale. Elles doivent etre activees deliberement.
noUncheckedIndexedAccess
Ajoute undefined au type retourne par les acces par index sur des tableaux et objets indexes. Tres efficace pour eviter les crashes sur des indices hors-limites.
// Sans noUncheckedIndexedAccess : TypeScript fait confiance a l'index
const fruits: string[] = ['pomme', 'poire'];
const first: string = fruits[0]; // Type: string (mais peut etre undefined)
const tenth: string = fruits[9]; // Type: string — mais c'est undefined en runtime !
// Avec noUncheckedIndexedAccess : TypeScript ajoute undefined
const first: string | undefined = fruits[0]; // Type honnete
// Obligation de verifier avant utilisation
if (first !== undefined) {
console.log(first.toUpperCase()); // Acces sur sans car verifie
}
// Meme chose pour les objets indexes
const config: Record<string, number> = { timeout: 3000 };
const val: number | undefined = config['timeout']; // Desormais string | undefined
exactOptionalPropertyTypes
Distingue une propriete optionnelle absente d'une propriete explicitement mise a undefined. Empeche les assignations involontaires de undefined sur des proprietes optionnelles.
// Sans exactOptionalPropertyTypes : optionnel et undefined sont equivalents
interface Config {
timeout?: number; // Optionnel : peut etre absent ou undefined
}
const cfg: Config = { timeout: undefined }; // Accepte
// Avec exactOptionalPropertyTypes : distinction stricte
interface Config {
timeout?: number; // Optionnel signifie absent, pas undefined
}
const cfg: Config = { timeout: undefined };
// Erreur : Type 'undefined' is not assignable to type 'number'
// Parce que 'timeout?: number' signifie que la propriete peut etre absente
// mais si elle est presente, elle DOIT etre un number
// Correct : omettre la propriete si pas de valeur
const cfg1: Config = {}; // Propriete absente : OK
const cfg2: Config = { timeout: 3000 }; // Propriete presente avec valeur : OK
noPropertyAccessFromIndexSignature
Interdit l'acces via la notation point (obj.prop) sur les types qui utilisent un index signature. Force l'utilisation de la notation crochet (obj['prop']) pour signaler explicitement que l'acces peut etre absent.
// Sans noPropertyAccessFromIndexSignature
interface DynamicConfig {
[key: string]: string; // Index signature : cles dynamiques
version: string; // Propriete connue
}
const cfg: DynamicConfig = { version: '1.0', apiUrl: 'https://api.example.com' };
cfg.apiUrl; // Accepte malgre le risque (apiUrl est dans l'index signature)
// Avec noPropertyAccessFromIndexSignature : erreur
// Property 'apiUrl' comes from an index signature, so it must be accessed with ['apiUrl']
// Correction : utiliser la notation crochet pour les proprietes dynamiques
cfg['apiUrl']; // Explicite : signale que l'acces est potentiellement absent
cfg.version; // Toujours OK car 'version' est declaree explicitement
"strict": true dans votre tsconfig.json. Activez-les apres avoir stabilise le mode strict de base.
Migration progressive sur un projet existant
Activer "strict": true d'un coup sur un projet mature peut generer des centaines d'erreurs. La bonne approche est une migration par etapes, sans casser la base de code existante.
Etape 1 : mesurer l'impact avant de commencer
// 1. Activez strict dans tsconfig.json temporairement
// 2. Lancez la compilation sans emit pour compter les erreurs
npx tsc --noEmit 2>&1 | grep "error TS" | wc -l
// Exemple de sortie : 247 erreurs
// Cela vous donne une estimation du travail a prevoir
Etape 2 : activer les options une par une
Commencez par les options les moins perturbatrices, puis progressez vers les plus strictes :
// tsconfig.json — Phase 1 : les options les moins perturbatrices
{
"compilerOptions": {
// Etape 1 : activer ces deux options en premier
"alwaysStrict": true, // Injecte "use strict", peu d'impact
"strictBindCallApply": true, // Verifie .bind/.call/.apply
// Etape 2 : activer apres correction de l'etape 1
// "noImplicitAny": true,
// Etape 3 : la plus impactante, en dernier
// "strictNullChecks": true,
// "strictPropertyInitialization": true,
// "strictFunctionTypes": true,
// "noImplicitThis": true
}
}
Etape 3 : utiliser @ts-strict-ignore pour isoler les zones
// Pour les fichiers legacy difficiles a migrer immediatement,
// utilisez le commentaire de suppression TypeScript a la ligne
// Attention : a utiliser avec parcimonie, pas comme solution permanente
function legacyFunction(data: any) { // eslint-disable-line
// @ts-ignore -- TODO: typer correctement lors du sprint 3
return data.user.name;
}
// Mieux : utilisez la directive fichier pour exclure temporairement
// ts-nocheck desactive TypeScript pour tout le fichier
// @ts-nocheck
// A placer en premiere ligne du fichier a exclure temporairement
Etape 4 : patterns de correction frequents
// --- Correction de strictNullChecks ---
// Avant : retourne string | undefined implicitement
function findUser(id: number): User {
return users.find(u => u.id === id); // Peut retourner undefined !
}
// Apres : type honnete + gestion du cas absent
function findUser(id: number): User | undefined {
return users.find(u => u.id === id);
}
// Utilisation avec verification obligatoire
const user = findUser(42);
if (user !== undefined) {
console.log(user.name); // TypeScript autorise l'acces car verifie
}
// Ou avec l'operateur de coalescence nulle
const name = findUser(42)?.name ?? 'Inconnu';
// --- Correction de noImplicitAny dans les callbacks ---
// Avant : parametre implicitement any
const items = [1, 2, 3];
items.forEach(item => { // item: any implicite dans certains contextes
console.log(item.toFixed(2));
});
// Apres : type infere depuis le tableau ou explicite
items.forEach((item: number) => { // Type explicite
console.log(item.toFixed(2));
});
// Dans la pratique : TypeScript infere souvent le bon type
// depuis le contexte (ici number[] => item: number est infere)
// noImplicitAny ne pose probleme que quand l'inference echoue
tsconfig.json pour Angular
Un projet Angular a une configuration TypeScript specifique qui integre les besoins du compilateur Angular (NgC/Ivy), les alias de chemins et les options de performance.
Structure complete recommandee
// tsconfig.json — configuration racine pour Angular
{
"compileOnSave": false,
"compilerOptions": {
// --- Cible de compilation ---
"target": "ES2022", // Cible ECMAScript moderne (Angular 17+)
"module": "ES2022", // Format de module pour le bundler (esbuild/Vite)
"lib": ["ES2022", "dom"], // APIs disponibles : JS moderne + DOM navigateur
"useDefineForClassFields": false, // Compatibilite avec les decorateurs Angular
// --- Strict mode complet ---
"strict": true, // Active les 7 verifications strictes
"noUncheckedIndexedAccess": true, // Securise les acces par index
"exactOptionalPropertyTypes": true, // Distingue absent et undefined
"noPropertyAccessFromIndexSignature": true, // Force la notation crochet
// --- Options supplementaires recommandees ---
"noImplicitOverride": true, // Force le mot-cle 'override' sur les surcharges
"noFallthroughCasesInSwitch": true, // Interdit le fall-through implicite dans switch
"forceConsistentCasingInFileNames": true, // Evite les bugs d'import case-insensitif
// --- Resolution des modules ---
"moduleResolution": "bundler", // Mode de resolution optimise pour esbuild/Vite
"baseUrl": "./", // Racine pour les alias de chemins
"paths": {
// Alias de chemins : evite les imports relatifs profonds
"@core/*": ["src/app/core/*"], // Services, guards, interceptors
"@shared/*": ["src/app/shared/*"], // Composants/pipes partages
"@features/*": ["src/app/features/*"], // Modules fonctionnels
"@environments/*": ["src/environments/*"], // Variables d'environnement
"@models/*": ["src/app/core/models/*"] // Interfaces et types metier
},
// --- Output ---
"outDir": "./dist/out-tsc", // Dossier de sortie de la compilation
"sourceMap": true, // Genere les source maps pour le debug
"declaration": false, // Pas de fichiers .d.ts (pas une librairie)
"downlevelIteration": true, // Support correct des iterateurs ES6+ en ES5
// --- Interoperabilite ---
"esModuleInterop": true, // Imports CommonJS plus naturels
"allowSyntheticDefaultImports": true, // Compatible avec les modules sans export default
"resolveJsonModule": true, // Permet d'importer des fichiers .json
// --- Decorateurs (requis par Angular) ---
"experimentalDecorators": true, // Active les decorateurs TypeScript (Angular legacy)
"emitDecoratorMetadata": true // Emis les metadonnees pour l'injection de dependances
},
"angularCompilerOptions": {
// --- Options specifiques au compilateur Angular (Ivy) ---
"enableI18nLegacyMessageIdFormat": false, // Desactive l'ancien format i18n
"strictInjectionParameters": true, // Verifie les parametres d'injection DI
"strictInputAccessModifiers": true, // Verifie les modificateurs d'acces sur @Input
"strictTemplates": true // Active la verification de type dans les templates HTML
}
}
Alias de chemins en pratique
Les alias definis dans paths permettent de remplacer les imports relatifs profonds par des imports lisibles et stables.
// Sans alias : imports relatifs fragiles et illisibles
import { AuthService } from '../../../core/services/auth.service';
import { UserModel } from '../../../../core/models/user.model';
import { ButtonComponent } from '../../../shared/components/button/button.component';
// Avec les alias @core, @shared, @models configures dans tsconfig.json
import { AuthService } from '@core/services/auth.service';
import { UserModel } from '@models/user.model';
import { ButtonComponent } from '@shared/components/button/button.component';
// Les imports sont absolus, lisibles et ne cassent pas lors des refactorisations
"strictTemplates": true dans angularCompilerOptions est l'equivalent de "strict" pour les templates HTML Angular. Il verifie les liaisons de proprietes, les types d'evenements et les directives structurelles.
tsconfig par environnement
Un projet Angular standard utilise plusieurs fichiers tsconfig qui heritent d'un fichier de base. Cette architecture evite la duplication et permet d'adapter la configuration selon le contexte (build, tests).
Architecture des fichiers tsconfig
| Fichier | Utilise par | Role |
|---|---|---|
tsconfig.json |
IDE, editeurs | Configuration de base, completion et verification dans l'editeur |
tsconfig.app.json |
ng build |
Compilation de l'application pour la production |
tsconfig.spec.json |
ng test |
Compilation pour les tests unitaires (Jasmine/Jest) |
tsconfig.app.json
// tsconfig.app.json — configuration pour le build de l'application
{
"extends": "./tsconfig.json", // Herite de toute la config de base
"compilerOptions": {
"outDir": "./dist/out-tsc",
// Exclut les types de test du bundle de production
"types": [] // Ne pas inclure @types/jasmine dans le build prod
},
"files": [
// Points d'entree explicites de l'application
"src/main.ts" // Fichier de bootstrap Angular
],
"include": [
"src/**/*.d.ts" // Inclut les declarations de types custom
]
}
tsconfig.spec.json
// tsconfig.spec.json — configuration pour les tests unitaires
{
"extends": "./tsconfig.json", // Herite de la config de base
"compilerOptions": {
"outDir": "./dist/out-tsc",
// Types disponibles uniquement dans le contexte de test
"types": [
"jasmine" // Types Jasmine pour describe(), it(), expect()...
// "jest" // Remplacer par jest si vous utilisez Jest
]
},
"include": [
// Inclut tous les fichiers source et tous les fichiers de spec
"src/**/*.spec.ts", // Fichiers de tests unitaires
"src/**/*.d.ts" // Declarations de types custom
]
}
Ajouter un tsconfig pour les librairies
// tsconfig.lib.json — si votre projet est une librairie Angular (nx, ng-packagr)
{
"extends": "./tsconfig.json",
"compilerOptions": {
"outDir": "../../dist/out-tsc",
"declaration": true, // Genere les fichiers .d.ts pour les consommateurs
"declarationMap": true, // Source maps pour les declarations (debug en librairie)
"inlineSources": true // Embarque les sources dans les source maps
},
"exclude": [
"src/test-setup.ts",
"**/*.spec.ts", // Exclut les tests du build de la librairie
"**/*.test.ts"
]
}
tsconfig.json qui herite d'un tsconfig.base.json a la racine du workspace. Le principe d'heritage reste identique.
Erreurs courantes avec strict et comment les corriger
Voici les erreurs les plus frequentes rencontrees lors de l'activation du strict mode, avec leur cause et leur correction.
Object is possibly null or undefined
// Erreur : TS2531 — Object is possibly 'null'
const input = document.getElementById('username');
input.value = 'test'; // Erreur : input peut etre null si l'element n'existe pas
// Correction 1 : operateur d'assertion non-null (a utiliser avec precaution)
const input = document.getElementById('username')!; // '!' affirme que ce n'est pas null
input.value = 'test'; // TypeScript fait confiance a votre assertion
// Correction 2 : verification explicite (recommandee)
const input = document.getElementById('username');
if (input !== null) {
(input as HTMLInputElement).value = 'test'; // Acces sur avec cast precise
}
// Correction 3 : optional chaining (pour les cas ou l'absence est acceptable)
const value = document.getElementById('username')?.getAttribute('value') ?? '';
Property does not exist on type
// Erreur : TS2339 — Property 'xxx' does not exist on type
interface User {
id: number;
name: string;
}
const user: User = { id: 1, name: 'Alice' };
console.log(user.email); // Erreur : 'email' n'existe pas sur User
// Correction 1 : ajouter la propriete a l'interface
interface User {
id: number;
name: string;
email?: string; // Propriete optionnelle si pas toujours presente
}
// Correction 2 : type guard pour les objets dynamiques
function hasEmail(user: User): user is User & { email: string } {
return typeof (user as any).email === 'string'; // Type guard custom
}
if (hasEmail(user)) {
console.log(user.email); // TypeScript connait le type enrichi
}
Type any cause par les bibliotheques sans types
// Erreur : noImplicitAny sur une bibliotheque sans @types/
import * as someLib from 'some-library-without-types'; // Erreur implicite
// Correction 1 : installer les types si disponibles
// npm install --save-dev @types/some-library
// Correction 2 : creer une declaration locale (fichier src/types/some-library.d.ts)
// src/types/some-library.d.ts
declare module 'some-library-without-types' {
// Declarez uniquement ce que vous utilisez
export function process(input: string): string;
export interface Config {
timeout: number;
}
}
// Correction 3 : declaration minimale pour debloquer rapidement
declare module 'some-library-without-types'; // Declare le module comme 'any'
// A enrichir progressivement avec les vrais types
Classe Angular : propriete non initialisee
// Erreur tres frequente dans les composants Angular avec strictPropertyInitialization
@Component({ selector: 'app-user', template: '' })
export class UserComponent {
@Input() userId: number; // Erreur : pas initialisee
user: User; // Erreur : pas initialisee
private subscription: Subscription; // Erreur : pas initialisee
}
// Correction adaptee aux composants Angular
@Component({ selector: 'app-user', template: '' })
export class UserComponent implements OnInit, OnDestroy {
// @Input() : utiliser le '!' pour les inputs requis (Angular les fournit)
@Input({ required: true }) userId!: number; // '!' = definitely assigned
// Proprietes initialisees avec des valeurs par defaut
user: User | null = null; // Nullable jusqu'au chargement
// Subscription : initialisee avec Subscription.EMPTY comme valeur par defaut
private subscription: Subscription = Subscription.EMPTY;
ngOnInit(): void {
this.subscription = this.userService.getUser(this.userId).subscribe(
user => this.user = user
);
}
ngOnDestroy(): void {
this.subscription.unsubscribe(); // Toujours nettoyer
}
}
Erreur avec exactOptionalPropertyTypes et Angular
// Erreur subtile avec exactOptionalPropertyTypes dans les objets Angular
interface FormState {
isLoading?: boolean;
}
// Erreur : undefined n'est pas assignable a boolean quand exactOptionalPropertyTypes est actif
const state: FormState = {
isLoading: someCondition ? true : undefined // Erreur !
};
// Correction : omettre la propriete plutot que mettre undefined
const state: FormState = someCondition ? { isLoading: true } : {};
// Ou utiliser un type union explicite si undefined est semantiquement different
interface FormState {
isLoading?: boolean | undefined; // Declare explicitement que undefined est valide
}
! (non-null assertion) est un outil de dernier recours. Preferez toujours les verifications explicites ou les valeurs par defaut. Un ! dans le code est un signal que TypeScript ne peut pas verifier cette garantie — c'est votre responsabilite.
Conclusion
Le strict mode TypeScript transforme votre tsconfig.json en premiere ligne de defense contre les bugs. Comprendre chaque option — de strictNullChecks a noUncheckedIndexedAccess — vous permet d'adopter une strategie eclairee : activation complete sur les nouveaux projets, migration progressive et par etapes sur les projets existants.
Pour un projet Angular, la combinaison "strict": true + "strictTemplates": true + les alias de chemins via paths constitue une base solide qui accelere les refactorisations, reduit les bugs en production et ameliore la lisibilite du code pour toute l'equipe.
"strict": true des le debut d'un nouveau projet. Sur un projet existant, planifiez la migration par sprints : une option a la fois, toutes les erreurs corrigees avant de passer a la suivante. Les benefices sur la qualite et la maintenance sont mesurables des les premieres semaines.