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
void | never | |
|---|---|---|
| La fonction se termine | oui | non (exception ou boucle infinie) |
| Valeur à l'exécution | undefined | aucune : l'appel n'en produit jamais |
| Code après l'appel | atteignable | inatteignable |
| Assignable à d'autres types | seulement à void, unknown, any | à tous les types |
| Usage typique | callbacks, gestionnaires d'événements, fonctions à effets de bord | fail(), 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
| Type | Valeurs qu'il contient | Assignable à | Accepte |
|---|---|---|---|
unknown | toutes les valeurs | seulement unknown et any | tout |
any | toutes les valeurs | tout sauf never | tout |
never | aucune valeur | tout | seulement 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.