Les Unions Discriminées en TypeScript
C'est quoi une union discriminée?
Une union discriminée (Le terme est correcte? je sais pas, en anglais on nous de Discriminated Union) est une fonctionnalité de TypeScript qui permet la création d'un type pouvant représenter plusieurs possibilités ou variantes différentes. En attachant des discriminants à chaque variante, le système de type TypeScript peut aider à s'assurer que nous traitons tous les cas possibles de manière gracieuse.
Cette technique courante pour travailler avec des unions consiste à avoir un seul champ qui utilise des types littéraux que vous pouvez utiliser pour permettre à TypeScript de réduire le type courant possible.
Commençons par prendre un exemple, créeons un type 'NetworkState'.
Imaginez un type NetworkState
qui représente l'état d'une connexion réseau. Cet état peut être "loading" (en cours de chargement), "failed" (échec) ou "success" (succès). Chaque état possède ses propres propriétés :
type NetworkState = {
state: "loading"|"failed"|"success";
code?: number;
response?: {
title: string;
duration: number;
summary: string;
};
}
const network:NetworkState = {
state:"loading",
code: 201,
response:{
title:"Connexion etablie"
duration:300,
summary: "Un petit resume ici"
}
}
Comme vous pouvez le voir, nous avons une propriété commune, qui est censée être toujours présentes pour les 3 states, loading, failed, ou success. Nous avons également deux propriétés optionnelles, le code
, la reponse response
.
Prenons le cas de en détails:
Il y a 3 states : loading, failed ou success
Pour le loading on a aucune propriété à ajouter
Pour failed, on a juste besoin de la propriété code
et pour success on a besoin de l'objet response.
Nous utilisons ce type pour l'objet appelé network.
Alors c'est quoi le problème?
Problème des Unions Simples
Considérons l'objet suivant :
const network:NetworkState = {
state:"loading",
code: 201,
response:{
title:"Connexion etablie"
duration:300,
summary: "Un petit resume ici"
}
}
Trouvez-vous cela normal? Un peu haut j'ai expliqué le principe de notre exemple, donc quand le state est "loading" on n'a pas besoin d'une autre information mais dans cet exemple, nous avons le state qui est "loading" avec ce qui suit. Et personne ne va m'empêcher d'avoir aussi :
const network:NetworkState = {
state:"failed",
code: 209,
response:{
title:"Titre ici"
duration:300,
summary: "Un petit resume ici"
}
}
Et soyons sérieux, ça n'a aucun sens tout ça, alors si ça n'a pas de sens, comment résoudre ça?
C'est là qu'intervient l'union discriminée(discriminated union)
Résoudre le Problème avec les Unions Discriminées
Commençons par séparer nos états en types distincts :
D'abord on doit chercher à savoir s'il y a de propriétés qui sont présentent pour tous nos states, et dans notre cas il n'y en a pas, toute fois vous pouvez faire face cela dans certains cas.
type LoadingState = {
state:"loading"
}
type FailedState = {
state:"failed"
code:number
}
type SuccessState = {
state:"success",
response: {
title: string;
duration: number;
summary: string;
};
}
Maintenant qu'on a nos types séparés, on peut maintenant créé un type qui va forcer à respecter notre règle de base.
type NetworkState = LoadingState | FailedState | SuccessState
Voila, avec ça on force le respect de notre règle prédéfinie, reprenons notre exemple et voyons ce que ça donne
Vous voyez maintenant qu'il y a erreur Object literal may only specify known properties, and 'response' does not exist in type 'FailedState
'. A chaque state ses propriétés et si cela n'est pas respecté on aura une erreur.
Manipuler des propriétés différentes avec les Type Guards
Dans le cas où vous cherchez à savoir comment gérer toutes ces propriétés qui peuvent exister ou non car l'objet peut avoir différents states; C'est là qu'intervienne les gardes de Type(Type Guards).
C'est quoi les Type Guards?
ChatGPT : Les gardes de type (Type Guards) en TypeScript sont des mécanismes permettant de s'assurer qu'une variable correspond à un type spécifique au sein d'un bloc conditionnel. Ils fournissent des informations de type plus précises au compilateur TypeScript à l'exécution, ce qui permet de coder de manière plus sûre et précise.
Les Type Guards permettent de déterminer le type précis d'une valeur au sein d'une union discriminée.
Disons que vous voulez avoir une fonction qui retourne un résultat en fonction du State. Pour ça on aura quelque chose qui va ressembler à ça :
const getResult = (state: NetworkState) => {
switch (state.state) {
case "loading":
return "loading";
case "failed":
return `Error: ${state.code}`;
case "success":
return `title: ${state.response.title}, duration: ${state.response.duration}, summary: ${state.response.summary}`;
}
}
Dans cet exemple, le switch
utilise le discriminant state
pour déterminer le type exact de l'objet state
et accéder aux propriétés correspondantes.
Vous pouvez également utilisé le if
si vous le souhaitez :
const showResponse = (state: NetworkState) => {
if (state.state === "loading") {
console.log("loading")
} else if (state.state === "failed") {
console.log("failed")
} else if (state.state === "success") {
console.log("success")
}
}
Normalement, en utilisant l'instruction switch, ou une condition if, tant qu'il s'agit d'un bloc conditionnel, l'IDE n'affichera que les propriétés disponibles dans ce bloc.
Conclusion
Les unions discriminées constituent un outil puissant en TypeScript pour modéliser des données polymorphiques de manière sûre et efficace. Elles permettent de représenter des états d'application, des réponses API et d'autres structures complexes avec précision et rigueur. En les associant aux Type Guards, vous pouvez écrire du code TypeScript plus robuste et plus lisible.
Ressources
Subscribe to my newsletter
Read articles from John Katembue directly inside your inbox. Subscribe to the newsletter, and don't miss out.
Written by
John Katembue
John Katembue
I'm a web front-end developer based in Lubumbashi DRC. #ReactJs/NextJs, #TailwindCSS, #UnoCSS