TypeScript le système de types - 01 - any, unknown, never : le trio que personne ne comprend

Comprendre les différences entre any, unknown et never en TypeScript. Quand les utiliser, quand les éviter, et pourquoi any est un poison silencieux.

01 - any, unknown, never : le trio que personne ne comprend

Ce que tu vas apprendre

  • Pourquoi any détruit silencieusement ton typage
  • Comment unknown te protégé là où any te trahit
  • A quoi sert never (et pourquoi tu l'as deja utilise sans le savoir)
  • Les regles concrètes pour choisir entre les trois

Prerequisites

Avoir lu l'article d'introduction de cette serie.


any : la trappe de secours qui devient un trou dans la coque

Quand tu ecris any, tu dis au compilateur : "ne vérifié rien sur cette valeur". Ca compile, ca passe, et ca plante au runtime.

typescriptconst data: any = { name: "Nicolas" }
console.log(data.age.toString()) // compile ✅ — crash au runtime ❌

Le compilateur ne bronche pas. data.age est undefined, et .toString() sur undefined lance un TypeError. Exactement le genre d'erreur que TypeScript est cense empecher.

Le vrai danger de any, c'est la contamination. Il se propage à travers les appels de fonctions :

typescriptfunction getConfig(): any {
  return JSON.parse(readFileSync("config.json", "utf-8"))
}

const config = getConfig()
const port = config.server.port // type: any
const host = config.server.host // type: any
const timeout = port.split(":") // compile ✅ — .split() sur un number, crash ❌

A partir du moment ou getConfig retourne any, tout ce qui en decoule perd son typage. Le compilateur ne te previent plus de rien. Tu as TypeScript dans le nom de tes fichiers et JavaScript dans le comportement de ton code.

J'ai vu des projets ou un seul any dans une fonction utilitaire contaminait 200 fichiers. Le dev n'avait aucune idee que ses types etaient devenus fictifs.

Quand any est acceptable

Presque jamais. Les seuls cas ou je toléré any :

  • Migration progressive d'une codebase JavaScript (avec un // TODO: type this a cote)
  • Contournement temporaire d'un bug dans les types d'une lib tierce
  • Un catch ou tu dois manipuler une erreur dont le type est inconnu (et encore, unknown est mieux)

Si tu utilises any pour autre chose, c'est un signal que tu n'as pas encore trouve le bon type. L'article sur les generics couvre la plupart des cas ou les devs mettent any par reflexe.

unknown : le any qui te force a vérifier

unknown est l'oppose de any en termes de sécurité. Une valeur unknown peut contenir n'importe quoi, comme any. Mais tu ne peux rien en faire sans vérifier son type d'abord.

typescriptconst data: unknown = JSON.parse('{"name": "Nicolas"}')

// ❌ Erreur de compilation
console.log(data.name)

// ✅ Tu dois verifier d'abord
if (typeof data === "object" && data !== null && "name" in data) {
  console.log(data.name)
}

Le compilateur te force a prouver le type avant de l'utiliser. C'est exactement ce qu'on veut quand on recoit des donnees de l'extérieur : une réponse API, un fichier JSON, un localStorage.getItem, un body de requête HTTP.

unknown vs any en pratique

Prenons un parser de configuration :

typescript// ❌ Avec any — aucune protection
function parseConfig(raw: string): any {
  return JSON.parse(raw)
}
const config = parseConfig(input)
config.server.port // compile, mais peut crasher

// ✅ Avec unknown — le compilateur te guide
function parseConfig(raw: string): unknown {
  return JSON.parse(raw)
}
const config = parseConfig(input)

// Tu dois verifier la structure
if (
  typeof config === "object" &&
  config !== null &&
  "server" in config &&
  typeof config.server === "object" &&
  config.server !== null &&
  "port" in config.server
) {
  console.log(config.server.port) // type: unknown, mais au moins c'est safe
}

C'est plus verbeux. C'est fait expres. Si tu veux quelque chose de plus ergonomique pour la validation, la serie couvre Zod dans la sous-serie pratique. Mais le principe est la : unknown te force a écrire le code de validation que tu aurais du écrire de toute facon.

Depuis TypeScript 4.4, le compilateur infere unknown pour les variables de catch quand useUnknownInCatchVariables est active dans le tsconfig (inclus dans strict). Avant, err dans un catch etait any par défaut.

typescripttry {
  riskyOperation()
} catch (err) {
  // Avec strict: true, err est unknown
  if (err instanceof Error) {
    console.log(err.message) // ✅ safe
  }
}

never : le type de ce qui n'arrive jamais

never est le type le plus abstrait des trois. Il represente quelque chose qui ne peut pas exister. Une fonction qui lance toujours une exception retourne never. Un cas dans un switch qui ne devrait jamais etre atteint a le type never.

typescriptfunction throwError(message: string): never {
  throw new Error(message)
}

function fail(): never {
  while (true) {
    // boucle infinie — la fonction ne retourne jamais
  }
}

Ca peut sembler theorique. En pratique, never est ton meilleur allie pour l'exhaustivite.

Le pattern exhaustive check

Imagine un système de notifications avec trois canaux :

typescripttype Channel = "email" | "sms" | "push"

function send(channel: Channel, message: string) {
  switch (channel) {
    case "email":
      sendEmail(message)
      break
    case "sms":
      sendSms(message)
      break
    case "push":
      sendPush(message)
      break
    default: {
      const _exhaustive: never = channel
      throw new Error(`Canal inconnu : ${_exhaustive}`)
    }
  }
}

Si tu ajoutes "slack" a l'union Channel sans ajouter le case correspondant, le compilateur refuse. La variable channel dans le default aurait le type "slack", qui n'est pas assignable a never. Tu as un filet de sécurité a la compilation.

J'utilise ce pattern sur paltemps.fr pour tous les switchs sur des unions. Ca m'a sauve plusieurs fois quand j'ajoutais un nouveau type d'événement ou un nouveau statut de commande.

never dans le système de types

never est le "bottom type" de TypeScript. Il est assignable a tout, mais rien n'est assignable a never (sauf never lui-meme). C'est utile pour comprendre certains comportements :

typescripttype A = string & number // never — aucune valeur n'est a la fois string et number
type B = never | string  // string — never disparait des unions

Tu retrouveras never dans les conditional types pour filtrer des types. Par exemple, le type utilitaire Exclude fonctionne en transformant les types non voulus en never :

typescripttype Exclude<T, U> = T extends U ? never : T
type Result = Exclude<"a" | "b" | "c", "a"> // "b" | "c"

Le spectre des permissions

Pour résumer la relation entre les trois :

any        → tout est permis, rien n'est verifie
unknown    → rien n'est permis sans verification
never      → rien n'est possible (la valeur n'existe pas)

any est en haut du spectre (aucune contrainte). never est en bas (contrainte maximale). unknown est au milieu : la valeur existe, mais tu dois prouver son type.

En tant que regle personnelle : je n'utilise jamais any dans du code nouveau. unknown pour les donnees exterieures, never pour l'exhaustivite, et des types concrets pour tout le reste. Si le type est "trop complique", c'est un signal que j'ai besoin de generics, pas de any.


Résumé

  • any désactivé le compilateur et contamine les types en cascade — a éviter dans du code nouveau
  • unknown force la vérification du type avant utilisation — a utiliser pour toute donnee extérieure (API, JSON, user input)
  • never represente l'impossible et sert de filet de sécurité pour les verifications d'exhaustivite
  • Active strict: true dans ton tsconfig pour que catch utilise unknown par défaut

Article précédent : 00 - TypeScript ne se résumé pas a ajouter : string

Article suivant : 02 - Inference, widening et narrowing

Sources

Réservez un audit gratuit de 30 minutes. Je vous montre concrètement ce qu'on peut automatiser.