Menu

Narrowing en TypeScript : comment fonctionne l'analyse du flux

Le narrowing, c'est la façon dont TypeScript transforme un type large comme string | number en un type précis dans un if, un switch ou après un return anticipé. Toutes les formes de narrowing au même endroit : typeof, véracité, égalité, in, instanceof, assignations, prédicats de type et unions discriminées.

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

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

FormeExempleAffine
typeoftypeof x === "string"primitives et fonctions
Véracitéif (x)retire null, undefined et les littéraux falsy
Égalitéx === "a", x == null, x !== undefinedlittéraux, null, undefined
in"swim" in petunions d'objets, par propriété
instanceoferr instanceof TypeErrorinstances de classes
Array.isArrayArray.isArray(x)tableaux contre tout le reste
Assignationx = 5vers le type assigné
Prédicat de typefunction isUser(x: unknown): x is Usertout ce que vous savez vérifier
Fonction d'assertionfunction assertUser(x: unknown): asserts x is Usertout ce qui suit l'appel
Propriété discriminanteswitch (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.name affine obj.name (et obj["name"]), mais pas obj[key] quand key est une variable string et non un littéral, ni une copie faite avant la vérification.
  • Callbacks et réassignation. Dans un callback, une let affinée ne garde son affinage que si elle n'est pas réassignée après la création du callback. Une const, ou un paramètre jamais réassigné, reste affiné.
  • Vérifications cachées dans des helpers. Une fonction isString(x: unknown): boolean n'apprend rien au compilateur. Donnez-lui un type de retour prédicat, x is string, et ses appels affineront comme le fait typeof.

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).

Coddy programming languages illustration

Apprendre à coder avec Coddy

COMMENCER