Qualité du code#
TypeScript renforce la qualité du code en attrapant les erreurs de types à la compilation, mais ce n’est qu’une couche parmi plusieurs. Une base de code professionnelle s’appuie sur un pipeline de qualité complet : un linter pour faire respecter les conventions stylistiques et détecter les antipatterns, un formateur pour uniformiser la présentation, une suite de tests pour valider le comportement à l’exécution, et des outils de validation pour combler le fossé entre les types statiques et les données dynamiques reçues de l’extérieur. Ce chapitre présente chacun de ces outils et leur intégration dans un workflow TypeScript moderne.
ESLint avec TypeScript#
ESLint est le linter standard de l’écosystème JavaScript et TypeScript. Pour lui faire comprendre la syntaxe TypeScript et exploiter les informations de types, deux paquets sont indispensables.
Installation et configuration#
npm install --save-dev eslint @typescript-eslint/parser @typescript-eslint/eslint-plugin
Depuis ESLint 9, la configuration se fait dans un fichier eslint.config.js (format flat config), qui remplace les anciens .eslintrc.* :
// eslint.config.js
import tseslint from '@typescript-eslint/eslint-plugin';
import tsParser from '@typescript-eslint/parser';
export default [
{
files: ['**/*.ts', '**/*.tsx'],
languageOptions: {
parser: tsParser,
parserOptions: {
project: true, // Utilise le tsconfig.json le plus proche
tsconfigRootDir: import.meta.dirname,
},
},
plugins: {
'@typescript-eslint': tseslint,
},
rules: {
// Règles recommandées de base
...tseslint.configs.recommended.rules,
// Règles nécessitant le projet TypeScript (plus puissantes)
...tseslint.configs['recommended-requiring-type-checking'].rules,
},
},
];
Règles essentielles#
Certaines règles méritent une attention particulière :
rules: {
// Interdit les variables déclarées mais jamais utilisées
'@typescript-eslint/no-unused-vars': ['error', {
argsIgnorePattern: '^_',
varsIgnorePattern: '^_',
}],
// Interdit `any` explicite (préférer `unknown`)
'@typescript-eslint/no-explicit-any': 'error',
// Interdit les assertions de type non vérifiées (as Type)
'@typescript-eslint/no-unsafe-assignment': 'warn',
// Exige des promesses rejetées d'être gérées
'@typescript-eslint/no-floating-promises': 'error',
// Interdit d'attendre une valeur non-Promise avec await
'@typescript-eslint/await-thenable': 'error',
// Exige un type de retour explicite sur les fonctions exportées
'@typescript-eslint/explicit-module-boundary-types': 'warn',
}
Remarque 35
Les règles qui utilisent parserOptions: { project: true } ont accès au graphe de types complet du projet. Elles sont considérablement plus puissantes que les règles syntaxiques simples : @typescript-eslint/no-floating-promises peut par exemple détecter qu’une valeur retournée par une fonction asynchrone n’est pas attendue avec await, même si le code est syntaxiquement valide. En contrepartie, elles ralentissent légèrement le linting sur les gros projets.
Prettier#
Prettier est un formateur de code opinionated : il reformate entièrement le code selon ses propres règles, sans laisser de place aux débats stylistiques sur l’indentation, les guillemets ou les virgules finales. Il supporte TypeScript nativement.
Configuration#
npm install --save-dev prettier eslint-config-prettier
Le fichier .prettierrc configure les quelques options disponibles :
{
"semi": true,
"singleQuote": true,
"tabWidth": 2,
"trailingComma": "es5",
"printWidth": 100,
"arrowParens": "always"
}
eslint-config-prettier désactive toutes les règles ESLint qui pourraient entrer en conflit avec Prettier — car ESLint et Prettier ne doivent pas se contredire sur le style :
// eslint.config.js
import prettierConfig from 'eslint-config-prettier';
export default [
// ... configuration TypeScript-ESLint
prettierConfig, // doit être en dernier pour écraser les règles de style
];
Le formattage peut être intégré dans le workflow de commit via lint-staged et husky :
// package.json
{
"lint-staged": {
"*.{ts,tsx,vue}": ["eslint --fix", "prettier --write"]
}
}
Tests avec Vitest#
Vitest est un framework de tests unitaires conçu pour Vite. Il réutilise la configuration TypeScript du projet et propose une API compatible avec Jest, ce qui facilite la migration.
Configuration TypeScript#
// vitest.config.ts
import { defineConfig } from 'vitest/config';
export default defineConfig({
test: {
globals: true, // Expose describe, it, expect, vi globalement
environment: 'node', // ou 'jsdom' pour les tests de composants
coverage: {
provider: 'v8',
reporter: ['text', 'html'],
},
},
});
Typer les mocks avec vi.fn<Args, Return>()#
import { vi, describe, it, expect } from 'vitest';
// Mock d'une fonction simple
const mockFetch = vi.fn<[string], Promise<Response>>();
// Mock d'une dépendance de module
vi.mock('./serviceUtilisateur', () => ({
récupérerUtilisateur: vi.fn<[number], Promise<{ id: number; nom: string }>>(),
}));
import { récupérerUtilisateur } from './serviceUtilisateur';
describe('récupérerUtilisateur', () => {
it('retourne un utilisateur par son identifiant', async () => {
// Configurer le mock pour ce test
vi.mocked(récupérerUtilisateur).mockResolvedValueOnce({
id: 1,
nom: 'Alice',
});
const résultat = await récupérerUtilisateur(1);
expect(résultat.nom).toBe('Alice');
expect(récupérerUtilisateur).toHaveBeenCalledWith(1);
});
});
Tester des types avec @ts-expect-error#
On peut écrire des tests qui vérifient qu’une expression est bien une erreur de type :
describe('types stricts', () => {
it('ne doit pas accepter une chaîne là où un nombre est attendu', () => {
function additionner(a: number, b: number): number {
return a + b;
}
// Cette ligne doit provoquer une erreur TypeScript
// @ts-expect-error — argument de type string non assignable à number
additionner('1', 2);
// Si la ligne ci-dessus ne provoque PAS d'erreur, TypeScript signale
// que le @ts-expect-error est inutile — ce qui est aussi un test !
});
});
```{prf:definition} @ts-expect-error versus @ts-ignore
:label: definition-15-01
@ts-ignore supprime silencieusement toute erreur TypeScript sur la ligne suivante. @ts-expect-error est plus strict : il supprime l’erreur, mais signale lui-même une erreur si la ligne suivante ne provoque pas d’erreur TypeScript. C’est le comportement voulu dans les tests de types : si la contrainte que l’on teste disparaît (parce que le code a changé), le test échoue, ce qui permet de détecter les régressions de typage.
## Types stricts et options du compilateur
L'option `"strict": true` du `tsconfig.json` active un ensemble de sous-options. Les connaître individuellement permet de comprendre exactement ce que le compilateur vérifie.
```{prf:definition} Les sous-options de `strict`
:label: definition-15-02
`"strict": true` est un raccourci pour activer simultanément :
- **`strictNullChecks`** : `null` et `undefined` ne sont plus assignables à tous les types. C'est la vérification la plus impactante.
- **`strictFunctionTypes`** : vérifie la contravariance des paramètres de fonctions.
- **`strictBindCallApply`** : type les méthodes `bind`, `call` et `apply` correctement.
- **`strictPropertyInitialization`** : interdit les propriétés de classe non initialisées dans le constructeur.
- **`noImplicitAny`** : interdit les paramètres dont le type est implicitement `any`.
- **`noImplicitThis`** : interdit `this` dans les fonctions dont le type est `any`.
- **`useUnknownInCatchVariables`** : les variables capturées dans les clauses `catch` ont le type `unknown` plutôt que `any`.
- **`alwaysStrict`** : émet `"use strict"` dans tous les fichiers de sortie.
Options supplémentaires recommandées#
Au-delà de strict, plusieurs options augmentent encore la rigueur :
{
"compilerOptions": {
"strict": true,
// Accès aux tableaux et objets indexés : T | undefined (pas juste T)
"noUncheckedIndexedAccess": true,
// Interdit d'assigner undefined à une prop optionnelle absente
"exactOptionalPropertyTypes": true,
// Avertit quand une valeur de retour de fonction est ignorée
"noUnusedLocals": true,
"noUnusedParameters": true,
"noFallthroughCasesInSwitch": true,
"noImplicitOverride": true
}
}
```{prf:example} noUncheckedIndexedAccess en pratique
:label: example-15-01
Sans noUncheckedIndexedAccess, accéder à un élément d’un tableau retourne T, même si l’index peut être hors limites :
const noms: string[] = ['Alice', 'Bob'];
const premier: string = noms[0]; // OK sans l'option
const centième: string = noms[100]; // OK sans l'option — mais undefined à l'exécution !
// Avec noUncheckedIndexedAccess :
const premier: string | undefined = noms[0]; // Le type inclut undefined
if (premier !== undefined) {
console.log(premier.toUpperCase()); // Ici TypeScript sait que c'est un string
}
Cette option est particulièrement utile pour les accès à des propriétés d’objets indexés avec Record<string, T> : sans elle, TypeScript considère que record['clé_inexistante'] est de type T, alors que la valeur est undefined à l’exécution.
## Validation à l'exécution
### Pourquoi les types disparaissent à l'exécution
TypeScript est effacé lors de la compilation : les types n'ont aucune existence à l'exécution. Cela signifie que des données reçues de l'extérieur — une réponse d'API REST, un fichier JSON, des paramètres de formulaire — peuvent avoir n'importe quelle forme, indépendamment du type TypeScript qu'on leur attribue.
```typescript
interface Utilisateur {
id: number;
nom: string;
email: string;
}
// Ce cast ne vérifie RIEN à l'exécution
const utilisateur = await fetch('/api/utilisateurs/1')
.then(r => r.json()) as Utilisateur;
// Si l'API retourne { id: '1', nom: null }, TypeScript ne le sait pas
console.log(utilisateur.nom.toUpperCase()); // Crash potentiel à l'exécution
La solution est d’utiliser une bibliothèque de validation de schéma qui vérifie la structure des données à l’exécution et en infère les types TypeScript.
Zod — validation de schéma avec inférence#
Zod est la bibliothèque de validation la plus populaire dans l’écosystème TypeScript. Elle permet de décrire un schéma une seule fois et d’en dériver automatiquement le type TypeScript correspondant :
import { z } from 'zod';
const SchémaUtilisateur = z.object({
id: z.number().int().positive(),
nom: z.string().min(1).max(100),
email: z.string().email(),
rôle: z.enum(['admin', 'éditeur', 'lecteur']),
créé: z.string().datetime().optional(),
});
// Inférer le type TypeScript depuis le schéma (source unique de vérité)
type Utilisateur = z.infer<typeof SchémaUtilisateur>;
// Validation et parsing sécurisés
async function récupérerUtilisateur(id: number): Promise<Utilisateur> {
const données = await fetch(`/api/utilisateurs/${id}`).then(r => r.json());
// parse() lève une ZodError si les données ne correspondent pas au schéma
return SchémaUtilisateur.parse(données);
}
// Variante qui retourne un Result sans lever d'exception
async function essayerRécupérerUtilisateur(id: number) {
const données = await fetch(`/api/utilisateurs/${id}`).then(r => r.json());
const résultat = SchémaUtilisateur.safeParse(données);
if (!résultat.success) {
console.error('Données invalides :', résultat.error.flatten());
return null;
}
return résultat.data; // Type : Utilisateur
}
Remarque 36
Zod propose également la validation de tableaux (z.array(SchémaUtilisateur)), des unions discriminées (z.discriminatedUnion('type', [...])), des transformations (z.string().transform(s => s.trim())), et peut raffiner les validations existantes avec des règles métier arbitraires via .refine(). Sa popularité en fait un choix sûr avec un large écosystème d’intégrations (React Hook Form, tRPC, Fastify, etc.).
Valibot — une alternative légère#
Valibot est une alternative à Zod avec une empreinte bundle nettement inférieure, conçue pour être tree-shakeable :
import * as v from 'valibot';
const SchémaUtilisateur = v.object({
id: v.pipe(v.number(), v.integer(), v.minValue(1)),
nom: v.pipe(v.string(), v.minLength(1)),
email: v.pipe(v.string(), v.email()),
});
type Utilisateur = v.InferOutput<typeof SchémaUtilisateur>;
const résultat = v.safeParse(SchémaUtilisateur, données);
if (résultat.success) {
console.log(résultat.output.nom);
}
Analyse statique et CI#
tsc --noEmit dans le pipeline CI#
La commande tsc --noEmit lance le compilateur TypeScript en mode vérification uniquement : elle détecte toutes les erreurs de types sans produire de fichiers de sortie. C’est la base de tout pipeline de qualité TypeScript :
# Dans le pipeline CI
npx tsc --noEmit
GitHub Actions#
Voici un workflow GitHub Actions complet qui enchaîne vérification des types, lint et tests :
# .github/workflows/qualite.yml
name: Qualité
on: [push, pull_request]
jobs:
qualite:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Installer Node.js
uses: actions/setup-node@v4
with:
node-version: '20'
cache: 'npm'
- name: Installer les dépendances
run: npm ci
- name: Vérification des types
run: npx tsc --noEmit
- name: Lint
run: npx eslint . --max-warnings 0
- name: Tests
run: npx vitest run --coverage
Résumé#
Ce chapitre a présenté le pipeline de qualité complet pour un projet TypeScript professionnel :
ESLint avec
@typescript-eslint: le parser et le plugin TypeScript permettent à ESLint de comprendre les types et d’appliquer des règles qui exploitent le graphe de types du projet. Les règlesno-floating-promisesetawait-thenablesont particulièrement précieuses.Prettier : un formateur opinionated qui élimine les débats stylistiques.
eslint-config-prettierévite les conflits avec ESLint.Vitest : framework de tests natif pour Vite, avec une API Jest-compatible.
vi.fn<Args, Return>()type précisément les mocks ;@ts-expect-errorpermet d’écrire des tests de régression sur les types eux-mêmes.Options strictes du compilateur :
strictactive huit vérifications essentielles ;noUncheckedIndexedAccessetexactOptionalPropertyTypesrenforcent encore la rigueur.Validation à l’exécution : les types TypeScript disparaissent à la compilation. Zod et Valibot comblent ce fossé en validant les données externes et en inférant les types correspondants, faisant du schéma la source unique de vérité.
CI :
tsc --noEmitdans un pipeline GitHub Actions garantit que chaque contribution respecte le contrat de types du projet.
Le chapitre suivant conclut ce livre par les bonnes pratiques générales : principes de structuration des types, patterns éprouvés, stratégie de migration JavaScript → TypeScript et ressources pour continuer à progresser.