unknown vs any en TypeScript : la différence une bonne fois pour toutes
Il y a en TypeScript deux façons de dire « je ne connais pas le type de cette valeur », et elles se ressemblent tellement en apparence que la plupart des débutants les croient interchangeables : any et unknown. Elles sont pourtant exactement opposées. L'une désactive le compilateur, l'autre le met en alerte maximale. Confondre les deux, c'est passer à côté de la moitié de l'intérêt de TypeScript ; cet article les met face aux mêmes situations, mesures à l'appui, pour que la différence s'installe une bonne fois pour toutes.
Méthode habituelle de ce blog : tous les extraits ont été compilés avec TypeScript 7.0.2 en mode strict et, quand ils compilent, exécutés avec Node.js 24 pendant la rédaction. Les messages d'erreur du compilateur et les plantages sont cités tels quels, en anglais et avec leur code. Si vous découvrez TypeScript, commencez plutôt par le projet todo-list en TypeScript ; revenez ensuite, cet article éclairera un choix que vous y avez déjà croisé.
Deux réponses opposées à la même question
La question est : que doit faire le compilateur quand une valeur peut être n'importe quoi ? Le JSON d'une API, le contenu d'un champ de formulaire relu du stockage, l'erreur d'un catch : autant d'endroits où le type n'est pas connu à l'avance. any répond « alors ne vérifie rien » : la valeur échappe au système de types, toutes les opérations sont permises, on verra bien à l'exécution. unknown répond « alors n'autorise rien » : la valeur est acceptée, mais aucune opération n'est permise tant qu'on n'a pas prouvé son type. La documentation officielle le formule en une phrase, dans les notes de version de TypeScript 3.0 qui ont introduit ce type : unknown est « the type-safe counterpart of any », son pendant sûr.
any reporte le problème à l'exécution, unknown le règle à la compilation.Le banc d'essai : cinq opérations, deux verdicts
Pour mesurer l'écart, j'ai soumis les deux types au même fichier : une valeur qui contient en réalité le nombre 42, sur laquelle je tente cinq opérations toutes absurdes pour un nombre. Version any d'abord :
const donnee: any = 42;
donnee.toUpperCase(); // appeler une méthode de chaîne
donnee.utilisateur.nom; // lire une propriété imaginaire
donnee(); // appeler 42 comme une fonction
const n: number = donnee; // affecter sans vérification
donnee + 1; // arithmétiqueVerdict du compilateur : aucune erreur, le fichier compile intégralement. Cinq opérations dont trois sont des plantages garantis, et TypeScript n'a pas dit un mot : c'est le contrat d'any, il a désactivé la vérification. Même fichier, en remplaçant any par unknown :
error TS18046: 'donnee' is of type 'unknown'. (× 4 : lignes 3, 4, 5 et 7)
error TS2322: Type 'unknown' is not assignable to type 'number'.Cinq opérations, cinq refus. Les quatre usages directs déclenchent TS18046, et l'affectation à une variable number est bloquée par TS2322 : contrairement à any, un unknown ne se déverse pas dans un type précis sans preuve. Les deux types acceptent tout en entrée ; toute la différence est à la sortie.
Et que se passe-t-il si on exécute la version any, puisqu'elle compile ? Node s'arrête sur la toute première ligne :
TypeError: donnee.toUpperCase is not a functionC'est le résumé le plus court de cet article : avec any, cette erreur explose chez l'utilisateur à l'exécution ; avec unknown, elle s'affiche dans votre éditeur avant même de sauvegarder.
Le vrai danger d'any : il est contagieux
Si any ne polluait que sa propre ligne, il serait un péché véniel. Le problème est qu'il se propage : toute valeur qui en sort devient any à son tour, et chaque variable qui la reçoit perd sa protection. Voici un scénario réaliste, une fonction qui lit une configuration JSON, avec une faute de frappe cachée dedans :
function lireConfig(): any {
return JSON.parse('{"theme":"sombre","taille":14}');
}
const config = lireConfig();
const theme = config.thme; // faute de frappe : "thme"
const taille = config.taille.toFixed(1);
const inverse = theme.toUpperCase();
console.log(taille, inverse);Le compilateur accepte tout : config est any, donc config.thme aussi, donc theme aussi. La faute de frappe, l'erreur la plus banale du monde, traverse trois variables sans déclencher la moindre vérification. À l'exécution, mesuré avec Node :
TypeError: Cannot read properties of undefined (reading 'toUpperCase')Notez où le plantage a lieu : pas sur la ligne de la faute (config.thme renvoie tranquillement undefined), mais deux lignes plus bas, au premier usage de la valeur. Sur un vrai projet, ces deux lignes peuvent être deux fichiers, et le message d'erreur ne mentionne ni thme ni la fonction fautive. C'est cette contagion silencieuse qui fait d'any un problème d'équipe et pas un choix personnel : un seul any en amont désarme le compilateur pour tout le monde en aval. Le même code avec lireConfig(): unknown refuse de compiler dès la ligne config.thme, avec le TS18046 vu plus haut : la faute de frappe n'a jamais l'occasion d'atteindre l'exécution.
Le catch : vous utilisez déjà unknown sans le savoir
Bonne nouvelle : si vous travaillez en mode strict, TypeScript vous a déjà mis unknown entre les mains. Dans un bloc catch, la variable d'erreur peut être absolument n'importe quoi (en JavaScript, on peut faire throw "zut" ou throw 42), et le mode strict la type donc en unknown. La preuve par l'erreur, mesurée sur ce code :
try {
JSON.parse("pas du json");
} catch (erreur) {
console.log(erreur.message);
// error TS18046: 'erreur' is of type 'unknown'.
}Le compilateur refuse erreur.message tant qu'on n'a pas prouvé que l'erreur est bien un objet Error. La vérification idiomatique tient en une ligne, et elle compile puis s'exécute sans broncher :
try {
JSON.parse("pas du json");
} catch (erreur) {
if (erreur instanceof Error) {
console.log("Erreur attrapée :", erreur.message);
}
}
// Erreur attrapée : Unexpected token 'p', "pas du json" is not valid JSONLa dernière ligne est la sortie réelle de Node. Ce petit détour par instanceof n'est pas une contrainte gratuite : il traite le cas, parfaitement légal, où quelqu'un a lancé autre chose qu'une Error, celui-là même qu'un any aurait laissé filer jusqu'au plantage.
Rétrécir un unknown : les gestes qui déverrouillent
Un unknown n'est pas une impasse, c'est une porte fermée dont le compilateur vous tend les clés. Chaque vérification à l'exécution (« narrowing », rétrécissement) déverrouille le type correspondant dans le bloc concerné. Une seule fonction suffit à montrer les quatre gestes courants :
function decrire(valeur: unknown): string {
if (typeof valeur === "string") {
return `un texte de ${valeur.length} caractères`;
}
if (typeof valeur === "number") {
return `le nombre ${valeur.toFixed(2)}`;
}
if (Array.isArray(valeur)) {
return `un tableau de ${valeur.length} éléments`;
}
if (valeur instanceof Date) {
return `une date : ${valeur.toISOString()}`;
}
return "autre chose";
}À l'intérieur du premier if, valeur est une string et .length compile ; dans le deuxième, c'est un number et .toFixed compile ; et ainsi de suite. J'ai exécuté la fonction sur cinq valeurs pour vérifier que la mécanique tient à l'exécution :
decrire("bonjour") → un texte de 7 caractères
decrire(3.14159) → le nombre 3.14
decrire([1, 2, 3]) → un tableau de 3 éléments
decrire(new Date("2026-08-24")) → une date : 2026-08-24T00:00:00.000Z
decrire(null) → autre chosePour les objets à la forme plus riche qu'un type primitif, le geste supplémentaire est le prédicat de type, une fonction qui vérifie champ par champ et dont la signature valeur is MonType informe le compilateur. C'est exactement le rôle de la fonction estUneTache construite dans le projet todo-list cité en introduction pour sécuriser la relecture du localStorage ; et si le <T> des signatures vous arrête encore, le guide des génériques complète le tableau. La logique est toujours la même : la vérification existe à l'exécution, le compilateur en tire les conséquences à la compilation.
Reste-t-il une place pour any ?
Mon avis, tranché : dans du code nouveau, aucune qui ne soit une dette assumée. Les deux seuls usages que je m'autorise sont la migration progressive d'un projet JavaScript, où any sert d'échafaudage temporaire le temps de typer module par module, et l'interfaçage avec une bibliothèque sans types corrects, en isolant le any dans une petite fonction qui renvoie un type propre, pour bloquer la contagion à la frontière. Dans les deux cas, c'est un pis-aller étiqueté comme tel, pas un choix de conception.
Partout ailleurs, la règle tient en une phrase : ce qui vient de l'extérieur entre en unknown, se vérifie, puis circule avec un vrai type. Le coût, quelques lignes de typeof et un prédicat de temps en temps, s'amortit à la première faute de frappe attrapée ; la contagion mesurée plus haut montre ce que coûte l'autre option. Et si un jour vous écrivez any « juste pour faire taire l'erreur », relisez le message que le compilateur affichait : neuf fois sur dix, il vous indiquait précisément la vérification qui manquait.