Mode strict dans TypeScript: qu'activer

1. Vue d’ensemble

"strict": true est la ligne la plus importante dans tsconfig.json. Mais beaucoup l’activent sans savoir vraiment ce qu’ils ont activé. Cet article passe en revue chaque flag qu’active strict, les erreurs concrètes que chaque flag détecte, et quelques flags supplémentaires qu’il vaut la peine d’ajouter. À la fin, on verra comment migrer un ancien projet vers strict sans avoir à corriger des milliers d’erreurs d’un coup.

2. strict est un groupe de flags

Le strict mode n’est pas une vérification isolée. C’est un interrupteur général qui active plusieurs flags à la fois :

{
  "compilerOptions": {
    "target": "ES2022",
    "module": "NodeNext",
    "strict": true
  }
}

Avec TypeScript 5.x et 6.x, strict: true est équivalent à :

{
  "compilerOptions": {
    "noImplicitAny": true,
    "strictNullChecks": true,
    "strictFunctionTypes": true,
    "strictBindCallApply": true,
    "strictPropertyInitialization": true,
    "strictBuiltinIteratorReturn": true,
    "noImplicitThis": true,
    "useUnknownInCatchVariables": true,
    "alwaysStrict": true
  }
}

Deux choses à retenir :

  • Chaque fois que TypeScript ajoute une nouvelle vérification « strict », par exemple useUnknownInCatchVariables en 4.4 ou strictBuiltinIteratorReturn en 5.6, cette vérification s’active automatiquement avec strict. Donc mettre à jour TypeScript peut faire émerger de nouvelles erreurs même si vous n’avez pas touché le code.
  • Vous pouvez activer strict puis désactiver un flag spécifique. Le flag explicite prévaut toujours sur le flag général.

TypeScript 6.0 a changé la valeur par défaut de strict en true. Néanmoins, vous devriez toujours spécifier ce flag explicitement dans tsconfig.json, pour que le lecteur du fichier de config n’ait pas à deviner.

Pour essayer les exemples ci-dessous, installez simplement TypeScript et exécutez tsc --noEmit :

npm install --save-dev typescript
npx tsc --noEmit

3. Les flags les plus importants

3.1. noImplicitAny

Quand TypeScript ne peut pas inférer le type, il assigne implicitement any. Ce flag transforme ce cas en erreur.

function calculateTotal(items) {
  return items.reduce((sum, item) => sum + item.price, 0);
}
error TS7006: Parameter 'items' implicitly has an 'any' type.

Sans ce flag, items est any. Dans ce cas, item.prise (faute de frappe) compile quand même sans problème. La correction est de déclarer le type explicitement :

interface OrderItem {
  name: string;
  price: number;
}

function calculateTotal(items: OrderItem[]): number {
  return items.reduce((sum, item) => sum + item.price, 0);
}

Ce flag ne bloque que any implicite. Vous pouvez toujours écrire any explicitement si vraiment nécessaire.

3.2. strictNullChecks

C’est le flag le plus précieux. Quand il est désactivé, null et undefined peuvent être assignés à n’importe quel type. Quand il est activé, ils deviennent des types à part et vous devez les gérer explicitement.

interface Customer {
  id: number;
  email: string;
}

const customers: Customer[] = [{ id: 1, email: "an@example.com" }];

function getCustomerEmail(id: number): string {
  const customer = customers.find((c) => c.id === id);
  return customer.email;
}
error TS18048: 'customer' is possibly 'undefined'.

Array.prototype.find retourne Customer | undefined. Sans ce flag, TypeScript ignore undefined, et l’erreur Cannot read properties of undefined ne s’affiche qu’à l’exécution. La correction :

function getCustomerEmail(id: number): string | undefined {
  const customer = customers.find((c) => c.id === id);
  return customer?.email;
}

La fonction retourne maintenant string | undefined. Le type de retour dit la vérité, donc l’appelant est forcé de gérer le cas où rien n’est trouvé.

Attention : Ne « corrigez » pas l’erreur avec l’opérateur non-null customer!.email. L’opérateur ! ne fait que réduire au silence le compilateur, il ne vérifie rien. Utilisez-le seulement si vous êtes certain que la valeur existe et qu’il y a une raison que le compilateur ne voit pas.

3.3. strictPropertyInitialization

Ce flag exige que chaque propriété d’une classe soit assignée, soit au moment de la déclaration, soit dans le constructeur. Il a besoin de strictNullChecks pour fonctionner.

class PaymentService {
  apiKey: string;
  retries = 3;

  constructor(private readonly baseUrl: string) {}
}
error TS2564: Property 'apiKey' has no initializer and is not definitely assigned in the constructor.

Vous avez trois solutions :

class PaymentService {
  apiKey: string;              // 1. assigner dans le constructeur
  webhookSecret?: string;      // 2. déclarer comme optionnel
  client!: HttpClient;         // 3. assignation définitive : « croyez-moi, ce sera assigné »

  constructor(apiKey: string) {
    this.apiKey = apiKey;
  }
}

interface HttpClient {
  post(url: string, body: unknown): Promise<unknown>;
}

L’approche 3 (!) est courante avec les frameworks qui utilisent l’injection de dépendances ou les décorateurs, comme Angular ou TypeORM. Là, la propriété est assignée après que le constructeur a terminé. Sauf dans ces cas, préférez l’approche 1 ou 2.

3.4. useUnknownInCatchVariables

En JavaScript, vous pouvez throw n’importe quoi, pas nécessairement une Error. Ce flag change le type de la variable dans catch de any à unknown.

async function chargeOrder(orderId: string): Promise<void> {
  try {
    await fetch(`/api/orders/${orderId}/charge`, { method: "POST" });
  } catch (err) {
    console.error(err.message);
  }
}
error TS18046: 'err' is of type 'unknown'.

Pour utiliser err, vous devez d’abord réduire le type :

catch (err) {
  const message = err instanceof Error ? err.message : String(err);
  console.error(`Charge failed: ${message}`);
}

3.5. strictFunctionTypes

Ce flag vérifie les paramètres des types de fonction en utilisant la contravariance. En simple : vous ne pouvez pas passer une fonction qui ne traite qu’un type étroit où on attend une fonction traitant un type plus large.

interface Animal { name: string; }
interface Dog extends Animal { bark(): void; }

const handleDog = (dog: Dog) => dog.bark();

const handleAnimal: (animal: Animal) => void = handleDog;
error TS2322: Type '(dog: Dog) => void' is not assignable to type '(animal: Animal) => void'.

Si l’assignation ci-dessus était acceptée, appeler handleAnimal({ name: "Mèo" }) planterait parce que l’objet passé n’a pas de bark. Il y a une exception à connaître : ce flag ne s’applique pas aux méthodes déclarées avec la syntaxe method(x: T): void dans une interface. C’est un choix délibéré de TypeScript pour maintenir la compatibilité avec les nombreuses API existantes.

3.6. Les trois autres flags

Ces trois flags causent rarement des erreurs dans le nouveau code, mais il est bon de les connaître :

function formatPrice(amount: number, currency: string): string {
  return `${amount.toFixed(2)} ${currency}`;
}

formatPrice.call(null, "100", "VND"); // strictBindCallApply

function onClick() {
  console.log(this.id);               // noImplicitThis
}
error TS2345: Argument of type 'string' is not assignable to parameter of type 'number'.
error TS2683: 'this' implicitly has type 'any' because it does not have a type annotation.
  • strictBindCallApply vérifie les paramètres quand vous appelez call, apply et bind.
  • noImplicitThis signale une erreur quand this a un type any implicite. Vous le corrigez en déclarant un paramètre fictif function onClick(this: HTMLButtonElement).
  • alwaysStrict émet "use strict" dans chaque fichier en sortie et analyse le code en mode strict de JavaScript. Avec les modules ES, le mode strict est déjà par défaut.

4. Des flags en dehors de strict

Beaucoup pensent que strict: true est le niveau le plus stricte. Ce n’est pas le cas. Les flags suivants se trouvent en dehors du groupe strict, mais sont très utiles pour les nouveaux projets :

{
  "compilerOptions": {
    "strict": true,
    "noUncheckedIndexedAccess": true,
    "exactOptionalPropertyTypes": true,
    "noImplicitOverride": true,
    "noImplicitReturns": true,
    "noFallthroughCasesInSwitch": true
  }
}

Parmi ceux-ci, noUncheckedIndexedAccess est le plus important. Par défaut, TypeScript considère que l’accès à un tableau par index ou à un objet par clé dynamique a toujours une valeur :

const stockBySku: Record<string, number> = { "SKU-1": 10 };

const quantity = stockBySku["SKU-999"];
console.log(quantity.toFixed(0));

Sans ce flag, quantity a le type number et le code ci-dessus plante à l’exécution. Quand on active le flag, quantity a le type number | undefined :

error TS18048: 'quantity' is possibly 'undefined'.

Les autres flags :

  • exactOptionalPropertyTypes : discount?: number signifie « la clé peut ne pas exister », pas « on peut assigner explicitement discount: undefined ».
  • noImplicitOverride : une méthode qui surcharge une méthode parent doit avoir le mot-clé override. Ainsi, si la classe parent renomme la méthode, la classe enfant signale une erreur au lieu de créer silencieusement une nouvelle méthode.
  • noImplicitReturns : chaque branche d’une fonction doit faire un return.
  • noFallthroughCasesInSwitch : empêche d’oublier break dans un switch.

Attention : noUncheckedIndexedAccess est assez bruyant. La boucle for (let i = 0; i < arr.length; i++) fait aussi que arr[i] a le type T | undefined. Préférez for...of ou arr.forEach. Pour un grand projet existant, activez ce flag en dernier.

5. Vérifier le comportement avec les tests

La vérification de type s’exécute à la compilation, mais vous devriez quand même tester à la fois le comportement à l’exécution et le type de retour. Vitest supporte les deux :

npm install --save-dev vitest
// customer.ts
export interface Customer { id: number; email: string; }

const customers: Customer[] = [{ id: 1, email: "an@example.com" }];

export function getCustomerEmail(id: number): string | undefined {
  return customers.find((c) => c.id === id)?.email;
}
// customer.test.ts
import { describe, it, expect, expectTypeOf } from "vitest";
import { getCustomerEmail } from "./customer";

describe("getCustomerEmail", () => {
  it("retourne l'email si customer trouvé", () => {
    expect(getCustomerEmail(1)).toBe("an@example.com");
  });

  it("retourne undefined si rien trouvé", () => {
    expect(getCustomerEmail(42)).toBeUndefined();
  });

  it("le type de retour inclut undefined", () => {
    expectTypeOf(getCustomerEmail).returns.toEqualTypeOf<string | undefined>();
  });
});

Le troisième test est un test de type. Supposez que quelqu’un change plus tard la signature en : string et utilise ! pour forcer le type. Dans ce cas, vitest --typecheck (ou tsc --noEmit sur le fichier de test) signalera une erreur, parce que le type de retour ne correspond plus à string | undefined.

npx vitest run --typecheck
 ✓ customer.test.ts (3 tests)
 Test Files  1 passed (1)
      Tests  3 passed (3)

6. Migrer un ancien projet vers strict

Activer strict: true sur une grande base de code peut générer des milliers d’erreurs. L’approche pratique est d’y aller par étapes.

Étape 1 : activez strict, mais désactivez temporairement les flags les plus bruyants.

{
  "compilerOptions": {
    "strict": true,
    "strictNullChecks": false,
    "strictPropertyInitialization": false,
    "useUnknownInCatchVariables": false
  }
}

noImplicitAny est généralement le plus facile à corriger, alors traitez-le d’abord. Chaque fois que vous terminez une correction de flag, supprimez la ligne qui le désactive. Le nouveau code écrit subira automatiquement toutes les vérifications qui sont activées.

Étape 2 : comptez les erreurs par flag pour évaluer l’effort.

npx tsc --noEmit --strictNullChecks true | grep -c "error TS"

Étape 3 : avec strictNullChecks, corrigez répertoire par répertoire. Marquez temporairement les endroits que vous n’avez pas eu le temps de corriger avec // @ts-expect-error, suivi d’une note d’explication. N’utilisez pas @ts-ignore. Raison : @ts-expect-error signale automatiquement une erreur quand la ligne suivante n’a plus d’erreur, donc vous ne risquez pas d’oublier un commentaire en trop.

// @ts-expect-error TODO(strict): customer peut être undefined, gérer en PR #482
const email: string = findCustomer(id).email;

Étape 4 : évitez les régressions en CI. Ajoutez tsc --noEmit au pipeline pour qu’aucun code créant de nouvelles erreurs de type ne puisse être fusionné.

Attention : n’utilisez pas skipLibCheck ou any à la pelle comme un moyen « d’activer strict rapidement ». skipLibCheck: true ignore seulement la vérification des fichiers .d.ts, donc c’est acceptable. Mais éparpiller any partout annulera tous les bénéfices de la migration.

7. Conclusion

strict: true active neuf vérifications. Les deux vérifications qui offrent le plus de valeur sont noImplicitAny et strictNullChecks, car elles capturent la plupart des erreurs undefined is not a function et Cannot read properties of undefined à la compilation. Pour un nouveau projet, vous devriez activer strict ainsi que noUncheckedIndexedAccess, noImplicitOverride et exactOptionalPropertyTypes dès le début. Pour un projet existant, activez strict puis désactivez temporairement chaque flag, corrigez progressivement du plus facile au plus difficile, et utilisez la CI pour que le nombre d’erreurs ne fasse que diminuer.

typescripttsconfigstrict modetype safety