Le mode strict s'active en une ligne dans le tsconfig. Et là, le compilateur se met à hurler, avec des soulignements rouges un peu partout dans le projet.
Le réflexe le plus courant consiste à faire taire le bruit. Un any ici, un as là, un point d'exclamation pour forcer le passage. L'erreur disparaît de l'écran, le rouge s'efface, et le fichier compile.
Une erreur de typage est rarement un caprice du compilateur. C'est une question légitime posée à ton code : cette valeur existe-t-elle toujours, que se passe-t-il si ce champ est absent, ce chemin d'exécution a-t-il été prévu. Faire taire l'alarme n'éteint pas le feu. Voici les erreurs TypeScript les plus fréquentes, ce que chacune dit du design en dessous, et comment corriger la cause plutôt que le symptôme.
Le problème : les échappatoires qui désarment le compilateur
TypeScript laisse plusieurs portes de sortie quand une erreur bloque. Chacune a un usage légitime, et chacune se retourne contre le projet dès qu'elle devient un réflexe.
La première porte, c'est any. Tu déclares qu'une valeur peut être n'importe quoi, et le compilateur cesse de la surveiller. Son défaut principal est la contagion : une variable typée any contamine ses retours, les objets qu'elle remplit et les fonctions qui la reçoivent. On croit isoler un cas gênant, on ouvre une brèche dans tout un pan du code.
La deuxième, c'est l'assertion as. Elle revient à dire au compilateur "fais-moi confiance, cette valeur est de ce type". Rien n'est prouvé, tout est affirmé. En cas d'erreur d'appréciation, TypeScript ne protège plus, puisqu'on lui a ordonné de regarder ailleurs.
La troisième, c'est l'opérateur de non-nullité, ce point d'exclamation qu'on colle dans user!.email pour garantir que la valeur existe. Encore une garantie donnée sans preuve. Le jour où user est nul, l'application plante à l'exécution, exactement le genre d'incident que TypeScript existe pour éviter.
Ces trois gestes partagent le même effet : ils débranchent le détecteur de fumée parce qu'il fait du bruit. La fumée vient d'un design qui décrit mal la réalité des données. C'est cette cause que la suite cherche à nommer.
Le principe : le type checker est un test qui tourne en continu
Chaque frappe au clavier déclenche une petite batterie de vérifications. Le compilateur ne se contente pas de râler, il pose une question sur chaque chemin que ton code peut emprunter. Une erreur de typage signale un chemin que ton modèle de données ne sait pas décrire proprement.
Le bon angle de lecture tient dans une formule connue des développeurs aguerris : rendre les états impossibles inexprimables. Si ton typage autorise la construction d'un objet qui ne devrait jamais exister, par exemple un utilisateur connecté sans identifiant, alors ton modèle ment sur la réalité, et tu passes ton temps à colmater des cas qui n'auraient jamais dû être possibles.
Face à une bagarre avec le type checker, retourne la question. Pas "comment faire taire cette erreur", mais "qu'est-ce que mon type laisse passer que la réalité interdit". La réponse pointe presque toujours une donnée mal structurée, une frontière mal gardée ou une responsabilité mal placée.
Cette posture rejoint une idée plus large sur le code maintenable, celle qu'un bon design rend les mauvaises utilisations difficiles à écrire. Elle se décline sous plusieurs formes dans notre hub des 20 principes de code maintenable. Le typage n'est qu'un moyen parmi d'autres de faire respecter ces règles, automatiquement, à chaque sauvegarde.
Les erreurs courantes et ce qu'elles révèlent
1. Le any qui se propage
Une réponse d'API stockée telle quelle, sans type déclaré, arrive en any. À partir de là, tout est permis, y compris les fautes de frappe.
const data = await response.json() // data est any
console.log(data.user.naem) // faute de frappe, aucune alerte
Ce que ça révèle : la forme de cette donnée n'a jamais été décidée. Le bon geste passe par unknown, qui oblige à vérifier avant d'utiliser, puis par une validation à la frontière de l'application, là où la donnée entre. Zod, Valibot ou un simple garde écrit à la main font ce travail.
type Reponse = { user: { nom: string } }
const data: unknown = await response.json()
const parsed = schema.parse(data) // validation à l'exécution
console.log(parsed.user.nom) // typé, vérifié
2. Le as qui ment
L'assertion de type est tentante parce qu'elle efface l'erreur sans effort. Elle ne vérifie rien pour autant. Quand la donnée réelle diffère de ce qui a été promis, le bug surgit plus loin, déconnecté de sa cause, et le temps passé à remonter la piste dépasse largement celui qu'on croyait gagner.
const user = data as User // mensonge si data ne respecte pas User
Ce que ça révèle : il manque une validation à l'endroit où la donnée entre dans le système. Au lieu d'affirmer, prouve. Un garde de type transforme une donnée inconnue en donnée vérifiée, et le besoin de mentir au compilateur disparaît de lui-même.
function estUser(v: unknown): v is User {
return typeof v === "object" && v !== null && "nom" in v
}
if (estUser(data)) {
console.log(data.nom) // typé grâce à une vérification réelle
}
3. Le point d'exclamation qui repousse le plantage
Quand TypeScript signale qu'une valeur peut être nulle, il a souvent raison. Le forcer avec ! ne change pas la réalité, ça déplace juste le moment de l'explosion.
function bienvenue(user: User | null) {
return "Bonjour " + user!.nom // plante si user est null
}
Ce que ça révèle : un cas possible n'a pas été traité. Soit tu le gères explicitement, soit tu restructures pour que la valeur ne puisse pas être nulle à cet endroit. Demander la garantie à l'appelant plutôt que la supposer rejoint l'esprit du principe Tell Don't Ask.
function bienvenue(user: User | null) {
if (!user) return "Bonjour"
return "Bonjour " + user.nom // TypeScript sait que user existe
}
4. Property does not exist : un objet qui mélange plusieurs états
Voici le cas qui revient le plus souvent en formation. Un objet représente une requête réseau, avec un champ pour les données, un pour l'erreur, un pour le chargement. TypeScript proteste sur data parce qu'il peut être absent, on enchaîne les vérifications, et ça reste fragile.
// Modèle flou : tout est optionnel en même temps
type Etat = {
chargement: boolean
data?: Resultat
erreur?: string
}
Ce que ça révèle : le type autorise des combinaisons absurdes, comme un chargement en cours avec une erreur et des données présentes. Une union discriminée ne laisse exister qu'un état à la fois.
type Etat =
| { statut: "chargement" }
| { statut: "succes"; data: Resultat }
| { statut: "erreur"; message: string }
Dès que tu testes statut === "succes", TypeScript sait que data existe, sans aucune assertion. Les états impossibles deviennent inexprimables, et l'erreur de typage s'en va parce que le design est correct, pas parce qu'elle a été masquée.
Le bénéfice se prolonge dans l'interface. Avec un switch sur le champ discriminant, le compilateur vérifie que chaque cas est traité, et il proteste le jour où un nouvel état s'ajoute au type sans être géré dans le rendu. Ce filet évite les écrans blancs quand une équipe fait évoluer un modèle de données sans prévenir tout le monde.
5. Tout est string
Un identifiant utilisateur et un identifiant produit se mélangent sans que rien ne bronche, parce que les deux sont des string. Les types coïncident, le compilateur se tait, et le bug voyage tranquillement jusqu'en production.
function chargerUser(id: string) { /* ... */ }
chargerUser(produitId) // accepté à tort : tout est string
Ce que ça révèle : les types décrivent la technique et pas le métier. Deux identifiants qui ne sont pas interchangeables dans la vraie vie ne devraient pas l'être dans le code. Un type nommé, même léger, rend la confusion impossible.
type UserId = string & { readonly marque: unique symbol }
function chargerUser(id: UserId) { /* ... */ }
chargerUser(produitId) // erreur de typage, comme attendu
Lire un message d'erreur sans paniquer
Les messages de TypeScript ont mauvaise réputation, surtout quand des types génériques s'empilent. Un message de vingt lignes reste pourtant lisible si on l'attaque par le bon bout : la première ligne dit ce qui coince, les suivantes déroulent le chemin qui a mené là.
Prends une erreur classique du genre "Type 'string | undefined' is not assignable to type 'string'". Traduite en français courant, elle dit que la valeur peut être absente alors que la destination exige une valeur présente. La question à se poser derrière est celle du cas d'absence : d'où vient-il, et qu'est-ce que l'application doit faire quand il arrive. La réponse à ces deux questions produit le correctif, et ce correctif n'est jamais un point d'exclamation.
Quand un message devient illisible parce qu'un type générique est trop profond, une technique aide : extrais le type dans un alias nommé et survole-le dans l'éditeur. Le nom que tu es obligé de lui donner révèle souvent que ce type mélange deux idées, ce qui ramène au problème de design initial.
Les pièges à éviter
Désactiver strict pour avancer. Repasser en mode souple pour gagner du temps revient à éteindre le seul outil qui signale les problèmes avant tes utilisateurs. Mon avis, tranché : sur un projet destiné à durer, le mode strict n'est pas négociable, et la dette qu'il révèle existait déjà avant qu'on l'active.
Multiplier les gardes de type partout. Une même donnée vérifiée à dix endroits signale une validation placée au mauvais niveau. Valide une fois, à la frontière où la donnée entre, et le type reste sûr dans tout le reste du code. C'est le même arbitrage que celui de la factorisation au bon moment.
Sur-typer jusqu'à l'illisible. Les types conditionnels et les types mappés sont puissants, mais un type que personne ne comprend en relecture est un type raté. Un bon typage se lit comme une documentation. Quand une signature réclame trois lignes de commentaire pour être comprise, simplifie le design avant de complexifier le type.
Confondre une vraie erreur et un type tiers cassé. Certaines erreurs viennent d'un fichier de définitions externe imprécis, pas de ton code. Vérifie d'où vient le type avant de te remettre en cause, tout en résistant à la tentation de tout asserter dès qu'une dépendance pose souci.
Croire que TypeScript protège à l'exécution. Les types disparaissent à la compilation. Une donnée venue du réseau ou d'un formulaire peut ignorer complètement ton type sans que le compilateur en sache quoi que ce soit. La validation à l'exécution reste indispensable aux frontières de ton application.
Le typage comme outil de conception
Le glissement à opérer est court à formuler et long à intégrer. TypeScript cesse d'être un correcteur tatillon pour devenir un partenaire de conception. Avant d'écrire la logique, on modélise les données et on décrit les états possibles, seulement ceux-là, ce qui laisse au compilateur le soin de rappeler les cas oubliés.
Cette habitude produit un effet secondaire précieux : nommer correctement les états d'un objet révèle souvent une règle métier qu'on n'avait pas formulée. Le futur bug est corrigé avant d'avoir été écrit, ce qui reste la manière la moins coûteuse de le traiter. Les données bien typées sont aussi des données prévisibles, donc plus faciles à tester, comme expliqué dans notre guide sur les tests et le TDD pour débutant.
Pour installer ces réflexes, notre formation TypeScript pour les développeurs JavaScript part des bases du système de types et va jusqu'aux unions discriminées. Les patterns plus avancés sont travaillés dans la formation JavaScript avancé, et leur mise en pratique dans une interface complète avec la formation React.
Questions fréquentes
Quand est-il acceptable d'utiliser any ?
Quelle différence entre as et un garde de type ?
Pourquoi activer le mode strict dès le début ?
TypeScript protège-t-il mes données à l'exécution ?
Qu'est-ce qu'une union discriminée et pourquoi ça aide ?
Faut-il typer chaque variable manuellement ?
Une piste pour ta prochaine session de code : cherche les as de ton projet et regarde d'où vient la donnée juste au-dessus. Dans la majorité des cas, tu vas tomber sur une frontière mal gardée, et c'est là que le travail commence.