Aller au contenu principal

TypeScript : les erreurs de typage qui révèlent un mauvais design

any, as, point d'exclamation : trois façons de faire taire le compilateur sans corriger quoi que ce soit. Cet article décode cinq erreurs de typage courantes et ce que chacune révèle du design en dessous, avec les correctifs en TypeScript, la lecture des messages d'erreur, et les cinq pièges à éviter au quotidien.

Outils & environnement ·
Adel LATIBI
Adel LATIBI

Le Briefing Dev - les ressources et actus de la semaine, droit dans ta boîte chaque vendredi gratuitement.

En vous inscrivant, vous acceptez de recevoir notre newsletter. Désinscription possible à tout moment.

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 ?

Presque jamais dans du code applicatif. Le besoin légitime le plus courant reste la migration progressive d'une base JavaScript existante, où l'on tolère temporairement des zones non typées. En dehors de ce cas, unknown force une vérification avant usage et fait mieux le travail. Un any qui reste en place devient une dette qui grandit avec le fichier.

Quelle différence entre as et un garde de type ?

L'assertion as affirme un type sans le vérifier, c'est une promesse faite au compilateur à tes risques. Un garde de type contrôle réellement la donnée à l'exécution et n'affirme le type que si le contrôle réussit. Aux frontières de l'application, utilise systématiquement la vérification plutôt que l'assertion.

Pourquoi activer le mode strict dès le début ?

Le mode strict active les vérifications les plus utiles, dont le contrôle des valeurs nulles, qui supprime à lui seul une grande part des plantages à l'exécution. L'activer tard sur un projet existant déclenche des centaines d'erreurs d'un coup, ce qui décourage et pousse aux raccourcis. Dès le départ, chaque erreur reste petite et locale.

TypeScript protège-t-il mes données à l'exécution ?

Non. Les types sont effacés à la compilation et n'existent plus quand le code tourne. Une réponse d'API ou un formulaire peut donc contredire ton type sans que rien ne l'empêche. La parade consiste à valider les données là où elles entrent, avec un garde de type ou une bibliothèque dédiée comme Zod.

Qu'est-ce qu'une union discriminée et pourquoi ça aide ?

C'est un type qui liste plusieurs formes possibles, chacune marquée par un champ commun qui les distingue, par exemple un champ statut. En testant ce champ, TypeScript déduit automatiquement les autres champs disponibles. Cela rend les états impossibles inexprimables et supprime le besoin d'assertions, ce qui en fait l'outil idéal pour modéliser un chargement, un succès ou une erreur.

Faut-il typer chaque variable manuellement ?

Non, et l'excès d'annotations nuit à la lecture. TypeScript déduit très bien le type des variables locales à partir de leur valeur. Réserve les annotations explicites aux endroits qui comptent : les signatures de fonctions, les contrats entre modules et les données qui entrent dans l'application.

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.

Crédits images : wirestock sur Magnific Tous les crédits

Vous êtes expert ?

Partagez votre expertise sur notre blog

Tutoriel, retour d'expérience, analyse - publiez un article invité et gagnez en visibilité.

Écrire pour nous