unknown, any et never dans TypeScript : lequel choisir

1. Vue d’ensemble

TypeScript possède trois types spéciaux qui prêtent à confusion : any, unknown et never. Tous trois apparaissent aux « limites » du système de type. any désactive la vérification de type, unknown vous oblige à vérifier avant d’utiliser, tandis que never représente un cas impossible.

À la fin de cet article, vous saurez quel type choisir pour les données d’origine inconnue. Vous saurez aussi comment utiliser never pour que le compilateur vous signale une erreur quand vous oubliez de traiter une branche.

Tous les exemples s’exécutent avec TypeScript 5.x et "strict": true dans tsconfig.json.

2. any : désactive la vérification de type

any permet à une valeur de faire n’importe quoi. Le compilateur ne vérifie rien :

let payload: any = "xin chào";

payload.toUpperCase();   // OK
payload.foo.bar();       // OK à la compilation, erreur à l'exécution
payload = 42;
const total: number = payload * 2; // OK

La troisième ligne compile sans problème. À l’exécution, elle lève une erreur :

TypeError: Cannot read properties of undefined (reading 'bar')

Avec any, TypeScript considère que vous êtes responsable. Vous perdez l’autocomplétion, les vérifications de noms de propriétés, et tous les bénéfices d’une refactorisation sûre.

2.1. any se propage

Le problème majeur avec any, c’est qu’il se propage à tout ce qu’il touche :

function loadConfig(): any {
  return JSON.parse('{"port": "8080"}');
}

const config = loadConfig();
const port = config.port;      // port: any
const next = port + 1;         // next: any
console.log(next);
80801

port est la chaîne "8080", donc port + 1 produit "80801" au lieu de 8081. Le compilateur n’avertit pas, car any s’est propagé de config à port puis à next. Un seul any au départ peut ruiner la vérification de type pour toute une chaîne d’appels.

Note : JSON.parse, response.json() dans l’API fetch et de nombreuses anciennes bibliothèques retournent any. C’est la source la plus courante de fuites de any dans le code réel.

3. unknown : sécurisé quand le type est inconnu

unknown accepte aussi toutes les valeurs comme any. La différence, c’est que vous ne pouvez pas utiliser la valeur tant que vous n’avez pas réduit son type (narrowing) :

let payload: unknown = "xin chào";

payload.toUpperCase();
error TS18046: 'payload' is of type 'unknown'.

Le compilateur bloque immédiatement. Vous devez d’abord prouver que payload est une chaîne :

if (typeof payload === "string") {
  console.log(payload.toUpperCase()); // payload: string
}
XIN CHÀO

À l’intérieur du bloc if, TypeScript sait que payload est une string, donc toUpperCase() est valide.

3.1. Moyens de réduire unknown

Il y a quatre outils principaux :

class PaymentError extends Error {
  constructor(message: string, public readonly code: string) {
    super(message);
  }
}

function describe(value: unknown): string {
  if (typeof value === "number") {
    return `Nombre : ${value.toFixed(2)}`;
  }
  if (value instanceof PaymentError) {
    return `Erreur de paiement ${value.code}`;
  }
  if (typeof value === "object" && value !== null && "id" in value) {
    return `Objet avec id : ${String(value.id)}`;
  }
  return "Inconnu";
}

console.log(describe(12.5));
console.log(describe(new PaymentError("Solde insuffisant", "INSUFFICIENT")));
console.log(describe({ id: 7 }));
console.log(describe(null));
Nombre : 12.50
Erreur de paiement INSUFFICIENT
Objet avec id : 7
Inconnu
  • typeof pour les types primitifs : string, number, boolean…
  • instanceof pour les classes.
  • in pour vérifier l’existence d’une propriété. Depuis TypeScript 4.9, après "id" in value, value a le type object & Record<"id", unknown>.
  • Vous devez vérifier value !== null car typeof null === "object".

3.2. Type guard pour les données externes

Pour les objets complexes, écrivez une type guard, c’est-à-dire une fonction retournant value is T :

interface Order {
  id: string;
  customerName: string;
  total: number;
}

export function isOrder(value: unknown): value is Order {
  if (typeof value !== "object" || value === null) return false;
  const v = value as Record<string, unknown>;
  return (
    typeof v.id === "string" &&
    typeof v.customerName === "string" &&
    typeof v.total === "number"
  );
}

export function parseOrder(json: string): Order {
  const data: unknown = JSON.parse(json);
  if (!isOrder(data)) {
    throw new Error("Données de commande invalides");
  }
  return data; // data: Order
}

console.log(parseOrder('{"id":"A1","customerName":"Lan","total":250000}'));
{ id: 'A1', customerName: 'Lan', total: 250000 }

Ce qui est crucial, c’est la ligne const data: unknown = JSON.parse(json). Assigner explicitement le type unknown empêche le any de JSON.parse de se propager. À partir de là, le compilateur vous oblige à vérifier avant d’utiliser data.

Dans les gros projets, remplacez les type guards manuels par une bibliothèque de validation de schéma comme Zod. Le principe reste le même : les données entrantes sont unknown, elles ne deviennent un type spécifique qu’après validation.

3.3. unknown dans catch

Avec strict: true (en particulier le drapeau useUnknownInCatchVariables), la variable dans catch a le type unknown :

try {
  parseOrder("{}");
} catch (err) {
  // err: unknown
  const message = err instanceof Error ? err.message : String(err);
  console.log(message);
}
Données de commande invalides

C’est exact par rapport à la réalité. JavaScript permet de throw n’importe quoi, même une chaîne ou un nombre, vous ne pouvez donc pas supposer que err est toujours une Error.

4. never : le type sans valeur

never est le type vide. Aucune valeur n’appartient au type never. Il apparaît dans trois situations principales.

4.1. Fonction qui ne retourne jamais

Une fonction qui lève toujours une exception, ou qui exécute une boucle infinie, a le type de retour never :

function fail(message: string): never {
  throw new Error(message);
}

function getDiscount(code: string): number {
  if (code === "TET2026") return 0.2;
  return fail(`Code de réduction inexistant : ${code}`);
}

Comme never s’assigne à tout type, return fail(...) est valide dans une fonction retournant number. Le compilateur sait aussi que le code après l’appel de fail() est inaccessible.

4.2. Vérification exhaustive

C’est la façon la plus utile d’utiliser never. Supposez que vous ayez une union de méthodes de paiement :

type PaymentMethod =
  | { kind: "card"; last4: string }
  | { kind: "cash" }
  | { kind: "momo"; phone: string };

export function assertNever(value: never): never {
  throw new Error(`Cas non géré : ${JSON.stringify(value)}`);
}

export function paymentLabel(method: PaymentMethod): string {
  switch (method.kind) {
    case "card":
      return `Carte **** ${method.last4}`;
    case "cash":
      return "Espèces";
    case "momo":
      return `MoMo ${method.phone}`;
    default:
      return assertNever(method);
  }
}

Après avoir traité les trois case, dans la branche default, method est réduit à never. L’appel à assertNever(method) est donc valide.

Maintenant, supposez qu’un collègue ajoute { kind: "zalopay"; phone: string } à l’union mais oublie de modifier le switch. Le compilateur signale immédiatement :

error TS2345: Argument of type '{ kind: "zalopay"; phone: string; }' is not assignable to parameter of type 'never'.

L’erreur est détectée à la compilation, plutôt que de laisser l’utilisateur ne voir qu’undefined sur l’écran de paiement. Si des données étrangères passent quand même, par exemple depuis une ancienne API, assertNever lève une erreur claire.

4.3. never dans les types conditionnels

Dans un type conditionnel, never sert à exclure un membre d’une union, puisque A | never est exactement A :

type OnlyStrings<T> = T extends string ? T : never;

type Mixed = "vnd" | "usd" | 404 | null;
type Currencies = OnlyStrings<Mixed>; // "vnd" | "usd"

TypeScript distribue le type conditionnel sur chaque membre de l’union. Les membres 404 et null deviennent never puis disparaissent. Les utility types intégrés comme Exclude, Extract et NonNullable fonctionnent tous selon ce mécanisme.

5. Comparaison rapide

La différence essentielle réside dans la relation d’assignation :

Assigner une autre valeur à ce type Assigner ce type à un autre Utiliser directement (appeler une méthode, accéder à une propriété)
any Oui Oui (sauf à never) Oui, pas de vérification
unknown Oui Seulement à unknown, any Non, narrowing requis
never Non (sauf never) Oui, vers n’importe quel type Pas de valeur à utiliser

On peut résumer comme suit :

  • unknown est le top type sécurisé. Tout est unknown.
  • never est le bottom type. never est un sous-type de tous les types.
  • any est une échappatoire. Il joue à la fois le rôle de top type et de bottom type, et casse donc la solidité du système de type.

6. Tester le comportement

Vous pouvez tester à la fois le comportement à l’exécution et les types à la compilation avec Vitest. Installation :

npm install -D vitest typescript

Supposez que les fonctions de la section 3.2 et 4.2 se trouvent dans order.ts et payment.ts :

// payment.test.ts
import { describe, it, expect, expectTypeOf } from "vitest";
import { isOrder, parseOrder } from "./order";
import { paymentLabel, assertNever } from "./payment";

describe("unknown et type guard", () => {
  it("accepte une commande valide", () => {
    const order = parseOrder('{"id":"A1","customerName":"Lan","total":1000}');
    expect(order.total).toBe(1000);
    expectTypeOf(order.total).toEqualTypeOf<number>();
  });

  it("rejette les données de mauvais type", () => {
    expect(isOrder({ id: "A1", customerName: "Lan", total: "1000" })).toBe(false);
    expect(() => parseOrder("null")).toThrow("Données de commande invalides");
  });
});

describe("never et vérification exhaustive", () => {
  it("retourne le bon libellé pour chaque méthode", () => {
    expect(paymentLabel({ kind: "card", last4: "4242" })).toBe("Carte **** 4242");
    expect(paymentLabel({ kind: "cash" })).toBe("Espèces");
  });

  it("lève une erreur avec des données en dehors de l'union à l'exécution", () => {
    const legacy = { kind: "paypal" } as unknown as never;
    expect(() => assertNever(legacy)).toThrow("Cas non géré");
  });
});
npx vitest run
 ✓ payment.test.ts (4 tests)
 Test Files  1 passed (1)
      Tests  4 passed (4)

expectTypeOf vérifie le type à la compilation. Si une modification quelconque fait que order.total devient any, le test échoue lors de la vérification de type (vitest --typecheck). Le dernier test simule des données « étrangères » d’un ancien système, pour s’assurer que le filet de sécurité à l’exécution fonctionne toujours.

7. Erreurs courantes et quand ne pas l’utiliser

Utiliser any pour « contourner » une erreur de compilation. C’est la pire habitude. Si vous ne connaissez vraiment pas le type, utilisez unknown. Activez la règle @typescript-eslint/no-explicit-any pour attraper ces cas.

Forcer le type d’unknown avec as au lieu de le réduire. data as Order rassure seulement le compilateur sans rien vérifier à l’exécution. Essentiellement, c’est aussi dangereux que any.

const data: unknown = JSON.parse('{"id": 1}');
const order = data as Order; // compile OK
order.customerName.trim();   // TypeError à l'exécution

Utiliser unknown pour tout. unknown est pour les limites du système : entrée utilisateur, réponse API, catch, données désérialisées. À l’intérieur du domaine, utilisez des types spécifiques. Une fonction calculateTotal(items: unknown) est une mauvaise conception.

Confondre never avec void. void signifie qu’une fonction retourne mais sans valeur utile. never signifie qu’une fonction ne retourne jamais. Une fonction log() classique doit être void, pas never.

Oublier la branche default dans un switch. Sans assertNever, TypeScript ne signale une erreur que si la fonction a un type de retour explicite et que noImplicitReturns est activé. Même dans ce cas, vous perdez le filet de sécurité à l’exécution.

Note : any a encore des cas d’usage valides. C’est quand vous migrez progressivement une grande base de code JavaScript, ou quand vous écrivez les déclarations pour une bibliothèque dont les types sont vraiment trop dynamiques. Confinez-le à un espace très réduit et laissez un commentaire expliquant pourquoi.

8. Conclusion

Ces trois types jouent trois rôles distincts :

  • Utilisez unknown pour les données de type inconnu aux limites du système, puis réduisez avec typeof, instanceof, in ou une type guard.
  • Utilisez never pour les fonctions qui lèvent toujours, pour la vérification exhaustive avec assertNever, et pour filtrer une union dans un type conditionnel.
  • Évitez any, sauf si vous avez une raison concrète et que vous l’avez confiné.

La règle pratique est très simple. Quand vous allez taper any, essayez d’abord unknown. Si le compilateur signale une erreur, ce sont exactement les endroits où vous devez valider les données.

typescripttype-safetyunknownneverany