Le narrowing, c'est TypeScript qui détermine un type plus précis pour une valeur à un endroit donné du code, à partir des vérifications que le code a déjà faites. Un paramètre string | number devient string dans if (typeof x === "string") et number dans le else.
Appeler value.toFixed(2) avant la vérification serait une erreur de compilation, car toFixed n'existe pas sur string. La vérification est du JavaScript ordinaire et s'exécute ; le narrowing, c'est le compilateur qui la lit et ajuste le type. Rien de plus n'est émis.
L'analyse du flux de contrôle
TypeScript suit chaque chemin d'une fonction : if/else, les return et throw anticipés, switch, les boucles, et les opérateurs de court-circuit &&, ||, ?? et ?:. À chaque endroit, le type d'une variable est ce qui y reste possible.
Le style à return anticipé (les « guard clauses ») est la façon la plus lisible d'affiner : traitez d'abord les cas particuliers, et le reste de la fonction travaille avec le type propre.
Toutes les façons d'affiner
| Forme | Exemple | Affine |
|---|---|---|
| typeof | typeof x === "string" | primitives et fonctions |
| Véracité | if (x) | retire null, undefined et les littéraux falsy |
| Égalité | x === "a", x == null, x !== undefined | littéraux, null, undefined |
in | "swim" in pet | unions d'objets, par propriété |
| instanceof | err instanceof TypeError | instances de classes |
Array.isArray | Array.isArray(x) | tableaux contre tout le reste |
| Assignation | x = 5 | vers le type assigné |
| Prédicat de type | function isUser(x: unknown): x is User | tout ce que vous savez vérifier |
| Fonction d'assertion | function assertUser(x: unknown): asserts x is User | tout ce qui suit l'appel |
| Propriété discriminante | switch (shape.kind) | unions étiquetées |
Les trois dernières sont traitées sur les pages type guards et unions discriminées. Les autres sont présentées ci-dessous.
Narrowing par véracité
if (x) retire null et undefined (ainsi que les types littéraux false, 0, ""). C'est court, avec un piège classique : 0 et "" sont falsy, donc des valeurs valides sont traitées comme absentes.
Pour les nombres et les chaînes, comparez explicitement à undefined ou null (ou utilisez ??). La véracité convient aux objets, aux tableaux et aux fonctions, qui ne sont jamais falsy.
Narrowing par égalité
===, !==, == et != affinent les deux côtés. Comparer à un littéral affine vers ce littéral ; == null (égalité large) correspond à la fois à null et à undefined en une seule vérification, et c'est le seul endroit où l'égalité large est idiomatique.
Comparer deux variables les affine toutes deux vers ce qu'elles peuvent avoir en commun : si a: string | number et b: string | boolean passent a === b, les deux sont string dans le if.
L'opérateur in
"key" in obj affine une union de types objet vers les membres qui ont (ou peuvent avoir) cette propriété.
in fonctionne aussi sur unknown une fois que vous savez que c'est un objet : après typeof v === "object" && v !== null && "id" in v, TypeScript sait que v possède une propriété id de type unknown. Pour les unions que vous concevez vous-même, une propriété étiquette commune (kind: "fish") est plus claire que de sonder la présence de méthodes : ce motif s'appelle une union discriminée.
Narrowing par assignation
Une variable a un type déclaré, et un type affiné qui suit ses assignations. Assigner une valeur l'affine vers le type de cette valeur, dans la limite du type déclaré.
Quand l'affinage se perd
Le narrowing est local et prudent. Quelques situations le réinitialisent :
- Une autre expression. Vérifier
obj.nameaffineobj.name(etobj["name"]), mais pasobj[key]quandkeyest une variablestringet non un littéral, ni une copie faite avant la vérification. - Callbacks et réassignation. Dans un callback, une
letaffinée ne garde son affinage que si elle n'est pas réassignée après la création du callback. Uneconst, ou un paramètre jamais réassigné, reste affiné. - Vérifications cachées dans des helpers. Une fonction
isString(x: unknown): booleann'apprend rien au compilateur. Donnez-lui un type de retour prédicat,x is string, et ses appels affineront comme le faittypeof.
Le compilateur signale index.ts(5,38): error TS18048: 'x' is possibly 'undefined'. Le callback pourrait s'exécuter plus tard, après x = undefined. Supprimez cette dernière assignation (ou copiez la valeur dans une const à l'intérieur du if) et le code compile et affiche 5 deux fois.
Un helper qui renvoie boolean se corrige en déclarant ce qu'il prouve. C'est cela, un type guard :
function isString(value: unknown): value is string {
return typeof value === "string";
}
Depuis TypeScript 5.5, le compilateur infère de tels prédicats pour les fonctions fléchées simples, c'est pourquoi list.filter((x) => x !== undefined) renvoie désormais un tableau sans undefined.
Questions fréquentes
Qu'est-ce que le narrowing de types en TypeScript ?
Le narrowing, c'est TypeScript qui précise le type d'une variable dans un bloc en fonction d'une vérification faite par le code. Après if (typeof x === "string"), un string | number n'est plus qu'un string dans le if et qu'un number dans le else. Le compilateur suit if, else, return, switch, &&, || et ?: pour déterminer le type à chaque endroit : c'est l'analyse du flux de contrôle.
Pourquoi TypeScript n'affine-t-il pas mon type ?
Causes fréquentes : la vérification porte sur une autre expression que celle que vous utilisez (obj.a vérifié, obj[key] utilisé avec un key de type string) ; la valeur est une let réassignée après la création d'un callback, si bien que le callback perd l'affinage ; ou la vérification est cachée dans un helper qui renvoie un simple boolean au lieu d'un prédicat de type x is T.
Le narrowing fonctionne-t-il à l'exécution ?
Les vérifications, oui : typeof, instanceof, in et === sont du JavaScript ordinaire qui s'exécute. Le narrowing lui-même n'existe qu'à la compilation. TypeScript lit vos vérifications et ajuste les types statiques en conséquence, et rien n'est ajouté au JavaScript émis.
Comment affiner un type unknown en TypeScript ?
Avec les mêmes vérifications : typeof value === "string", Array.isArray(value), value instanceof Date, ou pour les objets typeof value === "object" && value !== null && "id" in value. Pour des vérifications réutilisables, écrivez une fonction type guard avec un type de retour value is T.
Comment retirer undefined d'un tableau en TypeScript ?
items.filter((x) => x !== undefined) renvoie un T[] sans undefined depuis TypeScript 5.5, qui infère le callback comme un prédicat de type. Sur les versions plus anciennes, écrivez le prédicat vous-même : items.filter((x): x is T => x !== undefined).