Remplacer `any` de TypeScript : six modèles sûrs en termes de types pour les cas courants
Apprenez des alternatives pratiques et sûres en termes de types aux `any` de TypeScript — y compris unknown, les génériques, les unions discriminées et les vérifications exhaustives — pour gérer des données imprévisibles.
Sources: Stop Using ‘any’ in TypeScript — Here’s the Right Fix Every Time
Un petit changement de discipline qui vous épargne les surprises en production
Tout développeur TypeScript a déjà vécu ça. C’est tard un vendredi après-midi, une erreur de compilateur étrange bloque votre demande de fusion, et après vingt minutes à lutter avec des génériques imbriqués et des types union, tout ce que vous souhaitez, c’est déployer le code et éteindre votre ordinateur portable.
Alors vous saisissez ces trois lettres qui résolvent tout… ou rien :
const payload: any = response.data;
Le soulignement rouge disparaît. La compilation passe en vert. Tout semble normal.
Mais ce n’est pas correct. Utiliser any ne résout en rien le problème de saisie — cela indique à TypeScript d’arrêter de prêter attention. Vous avez ainsi désactivé le filet de sécurité qui vous avait poussé à choisir TypeScript en premier lieu. Chaque fois que any apparaît, cela crée une faille dans votre couverture de types, et cette faille se propage silencieusement à toutes les fonctions et variables qui entrent en contact avec elle.
La bonne nouvelle, c’est que vous n’en avez presque jamais vraiment besoin. Ci-dessous se trouve une explication du pourquoi any cause autant de dommages, suivie de solutions éprouvées pour les situations où les développeurs ont recours à cet élément le plus souvent.
Que signifie réellement any (et pourquoi cela a des effets néfastes)
Beaucoup de développeurs pensent que any signifie simplement « le type de cette valeur n’est pas encore connu ».
Ce que cela communique réellement au compilateur, c’est : « Cessez de vérifier cette variable et faites confiance au fait que tout ce que je fais avec elle par la suite est correct. »
let user: any = { name: "Alice" };
// TypeScript allows all of these without a single warning:
user.authenticate(); // Runtime crash: user.authenticate is not a function
user.address.postalCode; // Runtime crash: Cannot read properties of undefined
user(); // Runtime crash: user is not a function
Ce qui aggrave la situation, c’est l’usage de any. Une fois que le type de retour d’une fonction est any, toutes les variables qui capturent cette valeur de retour deviennent également any. En transmettant cette valeur à une autre fonction, la perte de sécurité continue de se propager. Bientôt, de grandes parties de votre codebase ne sont en réalité que du JavaScript pur portant une extension .ts.
Les sections suivantes présentent la solution appropriée pour chaque cas courant où any a tendance à s’infiltrer.
Réparation 1 : Lorsque le type n’est vraiment pas encore connu (unknown)
La situation : Vous récupérez des données depuis une API externe, vous analysez une chaîne JSON extraite du stockage local, ou vous traitez un en-tête brut provenant d’un webhook. Dans ces cas, il est impossible de connaître la forme exacte des données avant que le programme ne s’exécute réellement.
L’approche problématique :
function parseApiResponse(jsonString: string): any {
return JSON.parse(jsonString);
}
const data = parseApiResponse('{"id": 101}');
console.log(data.profile.avatar); // Compiles fine, crashes immediately if profile is missing
La correction appropriée : Utilisez unknown en combinaison avec un type guard.
unknown fonctionne comme une version plus sûre de any. Il indique au compilateur : « Le type de cette valeur n’a pas encore été déterminé, mais vous n’avez pas le droit d’y accéder par des propriétés, d’appeler des méthodes ou de l’exploiter d’une autre manière tant que vous n’avez pas prouvé ce qu’elle est réellement. »
interface UserProfile {
id: number;
name: string;
}
// Custom type guard
function isUserProfile(value: unknown): value is UserProfile {
return (
typeof value === "object" &&
value !== null &&
"id" in value &&
typeof (value as Record<string, unknown>).id === "number" &&
"name" in value &&
typeof (value as Record<string, unknown>).name === "string"
);
}
function parseApiResponse(jsonString: string): unknown {
return JSON.parse(jsonString);
}
const rawData = parseApiResponse('{"id": 101, "name": "Sarah"}');
// rawData.name; // Error: Object is of type 'unknown'.
if (isUserProfile(rawData)) {
// Inside this block, TypeScript knows rawData is definitely UserProfile
console.log(rawData.name); // Fully typed, autocompleted, and safe!
}
Si l’écriture de mécanismes de protection pour des charges utiles complexes vous semble trop contraignante, vous pouvez utiliser une bibliothèque de validation en temps de exécution telle que Zod ou Valibot. Ces bibliothèques valident les entrées inconnues et déduisent pour vous le type TypeScript correspondant en une seule étape.
Réparation 2 : Lorsqu’une fonction doit prendre en charge plusieurs types (génériques)
La situation : Vous développez une utilité réutilisable — pensez à un wrapper de mise en cache, à un outil pour les tableaux ou à une méthode sur un client API — conçue pour fonctionner avec de nombreuses structures de données différentes.
L’approche problématique :
function getFirstItem(items: any[]): any {
return items[0];
}
const numbers = [1, 2, 3];
const firstNum = getFirstItem(numbers); // firstNum is typed as 'any'
firstNum.toUpperCase(); // Compiles! Crashes at runtime because it's a number.
La correction appropriée : Utilisez les génériques (la syntaxe <T>).
Les génériques agissent comme des placeholders pour des types plutôt que pour des valeurs. Contrairement à any qui élimine les informations de type, un générique se souvient du type fourni par l’appelant et le conserve jusqu’à ce que la fonction renvoie une valeur.
function getFirstItem<T>(items: T[]): T | undefined {
return items[0];
}
const numbers = [1, 2, 3];
const firstNum = getFirstItem(numbers);
// TypeScript automatically infers firstNum as: number | undefined
const names = ["Alice", "Bob"];
const firstName = getFirstItem(names);
// TypeScript automatically infers firstName as: string | undefined
Vous pouvez également restreindre les possibilités d’un générique en utilisant extends, afin de garantir que le type possède certaines propriétés requises :
interface HasId {
id: string | number;
}
function printId<T extends HasId>(entity: T): void {
console.log(entity.id);
}
Réparation 3 : Gestion des erreurs à l’intérieur de try / catch
La situation : Vous écrivez de la logique à l’intérieur d’un bloc catch pour gérer un problème survenu.
L’approche problématique :
try {
fetchUserData();
} catch (err: any) {
// Risky: In JavaScript, anything can be thrown (strings, numbers, null)
console.error(err.message.toUpperCase());
}
Le risque ici est que JavaScript permet de lancer littéralement n’importe quoi — des chaînes, des nombres, null, des objets simples, et pas seulement des instances de Error. Si une bibliothèque ou une panne inattendue lance une simple chaîne, comme throw "Network timeout", alors err.message sera undefined, et l’appel à une méthode comme .toUpperCase() sur cette valeur peut provoquer une nouvelle erreur non gérée directement à l’intérieur du code censé gérer les erreurs.
La correction appropriée : Déclarer l’erreur capturée comme unknown et la restreindre en utilisant instanceof Error.
try {
fetchUserData();
} catch (err: unknown) {
if (err instanceof Error) {
// TypeScript safely knows 'err' has a .message property
console.error(`Error: ${err.message}`);
} else {
// Fallback for non-standard thrown objects
console.error("An unexpected error occurred", String(err));
}
}
Correction 4 : Objets dynamiques et recherches par clé
La situation : Vous avez besoin d’un objet qui se comporte comme un tableau de correspondances ou un dictionnaire, dont les clés ne sont connues qu’au cours de l’exécution du programme.
L’approche problématique :
const userRolePermissions: any = {};
userRolePermissions["admin"] = ["read", "write", "delete"];
La correction appropriée : Utiliser Record<Keys, Values>, ou à défaut une signature indexée.
type Role = "admin" | "editor" | "viewer";
type Permission = "read" | "write" | "delete";
// A strictly defined key-value mapping
const rolePermissions: Record<Role, Permission[]> = {
admin: ["read", "write", "delete"],
editor: ["read", "write"],
viewer: ["read"],
};
// If the keys can be arbitrary dynamic strings:
const sessionCache: Record<string, unknown> = {};
L’utilisation de Record<string, unknown> permet de conserver la structure de l’objet afin de pouvoir la vérifier, tout en obligeant quiconque le consomme à s’assurer de la nature réelle d’une valeur avant de l’utiliser.
Correction 5 : Déclarer les formes qui changent (Unions discriminées)
La situation : Un processus passe par différentes phases — comme idle, loading, success et error — et chaque phase contient son propre type de données.
L’approche problématique :
interface RequestState {
status: any;
data?: any;
error?: any;
}
Modéliser cela avec une seule interface floue signifie que chaque propriété devient optionnelle, vous forçant à utiliser des chaînes ?. partout dans votre code. Pire encore, rien ne vous empêche d’arriver à un état absurde où data et error sont tous deux remplis en même temps.
La solution correcte : recourir aux unions discriminées.
Donnez à chaque état possible un champ littéral commun — généralement nommé status ou type — que TypeScript peut utiliser pour distinguer les variantes :
type RequestState =
| { status: "idle" }
| { status: "loading" }
| { status: "success"; data: string[] }
| { status: "error"; error: Error };
function renderUI(state: RequestState) {
switch (state.status) {
case "idle":
return "Click start";
case "loading":
return "Loading...";
case "success":
// TypeScript automatically knows state.data exists here!
return `Loaded ${state.data.length} items`;
case "error":
// TypeScript automatically knows state.error exists here!
return `Failed: ${state.error.message}`;
}
}
Correction 6 : Vérification exhaustive avec never
La situation : vous souhaitez que le compilateur lui-même indique une erreur lorsque un collègue ajoute un nouveau cas à un type d’union mais oublie de mettre à jour toutes les chaînes switch ou if/else qui le gèrent.
L’approche problématique : Recourir à une solution générique default: return any, ou simplement ne pas gérer du tout ce cas particulier.
La correction appropriée : Attribuer la valeur restante au type never.
never décrit une valeur qui ne peut logiquement pas exister. Une fois que votre instruction switch a pris en compte tous les membres de l’union, tout ce qui reste dans la branche default est réduit au type never. Si quelqu’un étend par la suite l’union avec une nouvelle variante, le compilateur signale immédiatement une erreur à cette ligne, car la valeur restante ne correspond plus au type never:
type NotificationType = "email" | "sms" | "push";
function sendNotification(type: NotificationType) {
switch (type) {
case "email":
// Handle email
break;
case "sms":
// Handle SMS
break;
case "push":
// Handle push notification
break;
default: {
// If someone adds "in_app" to NotificationType and forgets to update this function,
// TypeScript will raise a compile-time error right here.
const _exhaustiveCheck: never = type;
return _exhaustiveCheck;
}
}
}
Référence rapide : Remplacer any
Comment le mettre en œuvre dans toute l’équipe
Comprendre ces remplacements ne résout que la moitié du problème — l’autre moitié consiste à empêcher any de réapparaître dès que la date limite approche.
Intégrez cette restriction directement dans la configuration de votre projet afin qu’elle soit appliquée automatiquement :
Dans tsconfig.json :
{
"compilerOptions": {
"strict": true,
"noImplicitAny": true
}
}
Dans votre configuration ESLint (.eslintrc.json) :
{
"rules": {
"@typescript-eslint/no-explicit-any": "error"
}
}
L’objectif principal de TypeScript est de vous donner confiance — confiance lors du refactoring, confiance lors du déploiement, ainsi qu’un complétion automatique dans l’éditeur qui vous épargne des recherches constantes dans la documentation. Éliminer any peut sembler contraignant les premiers jours. Mais une fois que unknown, les génériques et les unions discriminées prennent sa place, votre code commence à s’expliquer de lui-même, les erreurs en temps de exécution diminuent fortement, et le compilateur fonctionne enfin comme la protection pour laquelle il a été conçu.