Menu

Le type never en TypeScript : vérification exhaustive et void

never est le type sans aucune valeur. C'est le type de retour des fonctions qui ne se terminent jamais, le type qui reste quand le narrowing a écarté tous les cas, et l'outil derrière les vérifications exhaustives de switch. Découvrez d'où il vient et en quoi il diffère de void.

Cette page contient des éditeurs exécutables - modifiez, exécutez et voyez la sortie instantanément.

never est le type sans aucune valeur. Une fonction dont le type de retour est never ne se termine jamais normalement : elle lève une exception ou tourne indéfiniment. Une variable de type never marque du code qui ne peut pas s'exécuter, et c'est ce qui rend possibles les vérifications exhaustives.

return fail(...) compile dans une fonction qui renvoie number, car never est assignable à tous les types : une valeur de type never ne peut jamais réellement arriver.

Les fonctions qui ne se terminent jamais

Deux sortes de fonctions n'atteignent jamais leur fin : celle qui lève toujours une exception, et celle qui contient une boucle infinie. Le compilateur vérifie l'affirmation. Une fonction annotée never dont la fin est atteignable provoque l'erreur TS2534, A function returning 'never' cannot have a reachable end point.

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

function runForever(): never {
  while (true) {
    // poll, serve requests...
  }
}

L'inférence dépend de la syntaxe. Une déclaration de fonction qui ne fait que lever une exception est inférée comme renvoyant void, alors qu'une fonction fléchée ou une expression de fonction qui ne fait que lever une exception est inférée comme never :

function f1() { throw new Error("x"); }       // () => void
const f2 = () => { throw new Error("x"); };   // () => never

Le narrowing ne considère un appel comme une impasse que lorsque le nom appelé a un type explicite qui renvoie never : une déclaration de fonction annotée : never, comme fail plus haut, ou une variable avec une annotation de type, const fail: (m: string) => never = (m) => { throw new Error(m); }. Un never inféré ne compte pas, pas plus que const fail = (m: string): never => ..., où seule la fonction fléchée est annotée et pas la variable.

never ou void

voidnever
La fonction se termineouinon (exception ou boucle infinie)
Valeur à l'exécutionundefinedaucune : l'appel n'en produit jamais
Code après l'appelatteignableinatteignable
Assignable à d'autres typesseulement à void, unknown, anyà tous les types
Usage typiquecallbacks, gestionnaires d'événements, fonctions à effets de bordfail(), assertNever(), boucles infinies

La différence pratique se voit dans le narrowing. Après if (!user) fail("no user"), le compilateur sait que user est défini à la ligne suivante uniquement si fail renvoie never. Avec un retour void, il suppose que l'exécution peut continuer.

Vérifications exhaustives avec never

Chaque case d'un switch sur une union affine la valeur. Quand tous les membres ont été traités, ce qui reste dans default est never. L'assigner à une variable never transforme « j'ai traité tous les cas » en quelque chose que le compilateur vérifie :

Ajoutez maintenant un troisième membre à l'union sans ajouter de cas :

index.ts(18,26): error TS2345: Argument of type '{ kind: "triangle"; base: number; height: number; }' is not assignable to parameter of type 'never'.

L'erreur nomme le membre oublié. Ajoutez case "triangle": return (shape.base * shape.height) / 2; et le code compile à nouveau. Le helper assertNever est la forme réutilisable de la même vérification, et son throw compte toujours à l'exécution : des données venues de JSON ou d'un client plus ancien peuvent contenir un kind que les types déclarent impossible. Ce motif est la colonne vertébrale des unions discriminées.

Affiner jusqu'à never

La même chose se produit avec n'importe quel narrowing, pas seulement switch. Une fois toutes les possibilités écartées, la variable a le type never :

Si vous élargissez plus tard le paramètre à string | number | boolean | bigint, la ligne const nothing: never = x devient une erreur et désigne la fonction à mettre à jour.

never disparaît dans les unions

never est l'ensemble vide de valeurs, l'ajouter à une union ne change donc rien : string | never est simplement string. C'est ainsi que les types conditionnels filtrent les unions. Une branche qui renvoie never retire ce membre :

Les types intégrés Exclude<T, U> et Extract<T, U> fonctionnent exactement ainsi. Dans une intersection, c'est l'inverse : string & never vaut never.

Les types impossibles deviennent never

Une intersection qu'aucune valeur ne peut satisfaire se réduit à never :

type A = string & number;                   // never
type B = { kind: "a" } & { kind: "b" };     // never

Lire une propriété d'une valeur B indique la raison : Property 'kind' does not exist on type 'never'. The intersection 'B' was reduced to 'never' because property 'kind' has conflicting types in some constituents. Quand un type que vous avez construit se révèle être never, cherchez deux parties qui se contredisent.

never, unknown et any

TypeValeurs qu'il contientAssignable àAccepte
unknowntoutes les valeursseulement unknown et anytout
anytoutes les valeurstout sauf nevertout
neveraucune valeurtoutseulement never

unknown est le sommet de la hiérarchie des types et never sa base. any ne fait pas du tout partie de la hiérarchie : il désactive les vérifications.

Questions fréquentes

Qu'est-ce que le type never en TypeScript ?

never est le type qui n'a aucune valeur. Rien ne peut lui être assigné (sauf un autre never), et il est assignable à tous les types. Il apparaît comme type de retour des fonctions qui lèvent toujours une exception ou bouclent indéfiniment, comme type d'une variable après que le narrowing a écarté toutes les possibilités, et comme résultat de types impossibles tels que string & number.

Quelle est la différence entre never et void ?

Une fonction qui renvoie void se termine normalement ; elle ne renvoie simplement pas de valeur utile (à l'exécution, elle renvoie undefined). Une fonction qui renvoie never ne se termine pas du tout : elle lève une exception ou tourne indéfiniment. Le code situé après l'appel d'une fonction never est inatteignable, et TypeScript le traite ainsi lors du narrowing.

Comment faire une vérification exhaustive en TypeScript ?

Dans la branche default d'un switch sur une union, assignez la valeur à une variable de type never, ou passez-la à une fonction assertNever(value: never): never qui lève une exception. Si tous les cas sont traités, la valeur y est never et le code compile. S'il manque un cas, le compilateur signale que le membre manquant n'est pas assignable à never.

Pourquoi mon type est-il never ?

En général parce que TypeScript a écarté toutes les options par narrowing (par exemple après avoir vérifié typeof x === "string" et typeof x === "number" sur un string | number), ou parce qu'une intersection est impossible : string & number, ou deux types objet dont une propriété commune a des types littéraux contradictoires. Survolez le type dans l'éditeur pour voir quelle étape l'a produit.

Que signifie « is not assignable to type never » ?

Le code a essayé de placer une vraie valeur là où seul never est autorisé. Dans une vérification exhaustive, cela signifie qu'un membre de l'union n'a pas été traité. Ailleurs, cela veut souvent dire qu'un tableau a été inféré en never[] ou qu'une intersection s'est réduite à never ; ajoutez une annotation ou corrigez les types contradictoires.

Coddy programming languages illustration

Apprendre à coder avec Coddy

COMMENCER