TypeScript : le narrowing suit le raisonnement de votre code
Découvrez comment TypeScript resserre un type au fil de vos conditions, et comment décrire des états qui rendent les combinaisons incohérentes impossibles à écrire.
Où en êtes-vous avec cette notion ?
Une indication personnelle, enregistrée uniquement dans ce navigateur.
À lire ensuite
Le format café
L’essentiel à comprendre, le temps d’un café.
Envie de creuser ? Un cours plus complet vous attend juste après, à déplier sans quitter cette page.
Ce que vous allez comprendre
Comment TypeScript resserre un type au fil de vos conditions, et pourquoi bien décrire les états d’une interface supprime des bugs avant l’exécution. Les exemples ci-dessous sont du TypeScript : ils se lisent dans un éditeur ou se vérifient avec le compilateur, pas dans la console d’un navigateur.
Une condition renseigne le type
function afficher(valeur: string | number) {
if (typeof valeur === 'string') {
return valeur.toUpperCase();
}
return valeur.toFixed(2);
}
Avant le if, valeur peut être une chaîne ou un nombre : toUpperCase est refusé. À l’intérieur, TypeScript sait qu’il s’agit d’une chaîne. Après le if, il ne reste qu’une possibilité, number, donc toFixed est accepté. Vous n’avez déclaré aucun type supplémentaire : c’est le narrowing.
Une propriété qui départage les objets
type Compte =
| { type: 'user'; nom: string }
| { type: 'admin'; nom: string; permissions: string[] };
function decrire(compte: Compte) {
if (compte.type === 'admin') {
return compte.permissions.join(', ');
}
return compte.nom;
}
type n’est pas une chaîne quelconque : elle vaut exactement 'user' ou 'admin'. Cette valeur littérale permet au compilateur de choisir la bonne branche de l’union. On parle d’union discriminée.
Décrire des états, pas des options
Pour un appel d’API, la tentation est d’écrire { loading?: boolean; data?: User[]; error?: string }. Ce type autorise des états absurdes : chargement en cours, données présentes et message d’erreur, tout à la fois. Une union rend ces combinaisons impossibles à écrire.
type EtatApi =
| { statut: 'loading' }
| { statut: 'success'; donnees: string[] }
| { statut: 'error'; message: string };
Après avoir traité loading puis error, TypeScript sait que seul success reste : donnees existe forcément.
Le raccourci qui annule tout
compte as Admin ne vérifie rien. Vous affirmez, le compilateur vous croit, et si vous vous trompez le défaut n’apparaît qu’à l’exécution. Préférez une condition que le compilateur peut suivre.
Fiche mémo
typeof x === 'string'réduit un type primitif à l’intérieur de la branche.- Le
else, ou le code placé après unreturn, hérite de ce qui reste possible. - Union discriminée : une propriété littérale commune, souvent nommée
type,statutoukind. - Modélisez des états mutuellement exclusifs par une union, pas par des propriétés optionnelles.
asdésactive la vérification : à réserver aux cas où vous savez quelque chose que le compilateur ignore.
L’idée à emporter : TypeScript ne se contente pas des types déclarés, il suit vos conditions ; écrire des types qui rendent les états incohérents impossibles est le meilleur moyen d’en profiter.
Et si on allait plus loin ?
Le café vous a donné les repères. Prenez maintenant le temps de comprendre les mécanismes et de pratiquer, si vous le souhaitez.
Aller plus loinRendre l’oubli impossible : exhaustivité avec never et prédicats de typeDévelopper le coursReplier le cours
Ce que vous saurez faire
Obliger le compilateur à signaler tout cas d’union oublié, écrire un prédicat de type utilisable avec filter, et reconnaître les situations où le narrowing ne vous protège plus.
Contexte d’exécution : ces exemples ne tournent pas tels quels dans un navigateur. Placez-les dans un fichier .ts du projet et lancez npx tsc --noEmit, ou observez simplement les soulignements dans un éditeur configuré pour TypeScript. Le résultat attendu est un message du compilateur, pas une sortie console.
1. Le contrôle d’exhaustivité avec never
Le type never désigne une valeur qui ne peut pas exister. En affectant la variable examinée à une variable de ce type dans le cas default, vous demandez au compilateur de vérifier qu’il ne reste effectivement plus rien à traiter.
type Etat =
| { statut: 'loading' }
| { statut: 'success'; donnees: string[] }
| { statut: 'error'; message: string };
function rendre(etat: Etat): string {
switch (etat.statut) {
case 'loading':
return 'Chargement en cours';
case 'success':
return etat.donnees.join(', ');
case 'error':
return etat.message;
default: {
const jamais: never = etat;
return jamais;
}
}
}
Tel quel, ce code compile sans erreur. Dans le default, les trois cas ayant été traités, etat a le type never : l’affectation est légale.
Ajoutez maintenant un quatrième membre à l’union, par exemple | { statut: 'empty' }, sans ajouter de case. Le compilateur signale alors une erreur sur la ligne const jamais: never = etat; :
Type '{ statut: "empty"; }' is not assignable to type 'never'.
L’oubli est signalé à la compilation, à l’endroit exact, dans chaque fonction concernée.
Ce garde-fou suppose une union que le compilateur sait discriminer : avec un statut déclaré string, aucune réduction n’est possible.
Référence : narrowing.
2. Les prédicats de type, et ce qu’ils ne garantissent pas
Un prédicat de type transporte le résultat du narrowing hors de la fonction qui le calcule, pour une vérification réutilisable.
type User = { type: 'user'; nom: string };
type Admin = { type: 'admin'; nom: string; permissions: string[] };
type Account = User | Admin;
function estAdmin(compte: Account): compte is Admin {
return compte.type === 'admin';
}
function listerPermissions(comptes: Account[]): string[] {
return comptes.filter(estAdmin).map((admin) => admin.permissions.join(', '));
}
Le type de retour compte is Admin remplace boolean. Grâce à lui, comptes.filter(estAdmin) a le type Admin[], et l’accès à permissions est accepté. Avec un simple boolean, filter renverrait Account[] et l’accès serait refusé, ce qui pousse en général vers un as regrettable.
La limite est importante : c’est vous qui garantissez la véracité du prédicat. TypeScript vérifie que la fonction renvoie bien un booléen, il ne vérifie pas que ce booléen dit la vérité. Un corps réduit à return true; compilerait sans broncher et mentirait à tout le reste du programme. Un prédicat est donc un as documenté et centralisé : sa valeur dépend entièrement du soin apporté à son corps. Selon la version du compilateur, certains prédicats simples sont d’ailleurs déduits automatiquement, ce qui ne change rien à cette responsabilité.
3. Ce qui fait perdre le narrowing
La réduction de type est une lecture statique du code. Elle peut être invalidée par ce qui se passe entre le test et l’usage.
let valeur: string | number = 'texte';
function reinitialiser() {
valeur = 0;
}
if (typeof valeur === 'string') {
reinitialiser();
console.log(valeur.toUpperCase()); // accepté à la compilation
}
Le compilateur accepte généralement cet appel, alors que l’exécution échouera : reinitialiser a remplacé la chaîne par un nombre. Le narrowing raisonne sur le flux visible, pas sur les effets d’une fonction appelée entre-temps. Un paramètre ou une variable const n’expose pas ce risque : autant limiter les variables modifiables partagées.
Pièges et limites
typeof nullvaut'object'. Testertypeof x === 'object'n’élimine pasnull. Écrivezx !== null && typeof x === 'object', sans quoi la réduction obtenue est fausse.- Un prédicat erroné compile. Voir la section 2 : c’est la contrepartie de sa commodité.
- Le narrowing disparaît à l’exécution. Les types ne survivent pas à la compilation : rien ne vérifie qu’une réponse d’API correspond réellement à
Etat. Validez les données à la frontière du réseau. - Deux membres partageant la même valeur de discriminant ne peuvent plus être distingués. Chaque littéral doit être unique dans l’union.
À vous de jouer
Partez du type Etat de la section 1. Ajoutez un quatrième cas { statut: 'empty' } et faites en sorte que le compilateur signale les fonctions qui l’oublient. Écrivez ensuite une fonction qui, à partir d’un Etat[], renvoie toutes les données des états success, sans utiliser as.
Correction commentée
type Etat =
| { statut: 'loading' }
| { statut: 'success'; donnees: string[] }
| { statut: 'error'; message: string }
| { statut: 'empty' };
function rendre(etat: Etat): string {
switch (etat.statut) {
case 'loading':
return 'Chargement en cours';
case 'success':
return etat.donnees.join(', ');
case 'error':
return etat.message;
case 'empty':
return 'Aucun résultat';
default: {
const jamais: never = etat;
return jamais;
}
}
}
type Succes = Extract<Etat, { statut: 'success' }>;
function estSucces(etat: Etat): etat is Succes {
return etat.statut === 'success';
}
function toutesLesDonnees(etats: Etat[]): string[] {
return etats.filter(estSucces).flatMap((etat) => etat.donnees);
}
Pourquoi c’est la bonne réponse.
Le default avec never fait tout le travail de surveillance : entre l’ajout de 'empty' à l’union et celui du case correspondant, le compilateur refuse le fichier et pointe la ligne exacte. Vous n’avez pas à retrouver les fonctions à mettre à jour, elles se signalent seules.
Extract<Etat, { statut: 'success' }> récupère le membre concerné au lieu de le recopier. Si la forme de l’état success change plus tard, le prédicat suit automatiquement, sans duplication à maintenir.
estSucces teste le discriminant, c’est-à-dire exactement ce que son type de retour promet. La promesse est donc tenue, ce qui n’aurait pas été le cas d’un corps approximatif. Grâce à ce prédicat, filter renvoie Succes[] et flatMap accède à donnees sans conversion forcée.
Sans le prédicat, etats.filter((etat) => etat.statut === 'success') renvoie Etat[] : l’accès à donnees serait refusé, et le raccourci as réintroduirait le risque que ce cours cherche à éviter.
Référence : documentation TypeScript.
Vous pouvez aussi vous arrêter ici. L’approfondissement est facultatif.
Les repères Cours Café
Pour aller à la source
Documentation de référence. Vérification éditoriale encore à effectuer.
Gardez une trace de cette idée.
Favoris, notes et progression seront disponibles après connexion du stockage distant.