Français

Explorez les unions discrétisées TypeScript, un outil puissant pour construire des machines d'état robustes et type-safe. Apprenez à définir des états, gérer des transitions et exploiter le système de types TypeScript pour une fiabilité accrue du code.

Unions Discrétisées TypeScript : Construire des Machines d'État Type-Safe

Dans le domaine du développement logiciel, la gestion efficace de l'état de l'application est cruciale. Les machines d'état fournissent une abstraction puissante pour modéliser des systèmes complexes et état-dépendants, garantissant un comportement prévisible et simplifiant le raisonnement sur la logique du système. TypeScript, avec son système de types robuste, offre un mécanisme fantastique pour construire des machines d'état type-safe en utilisant des unions discrétisées (également connues sous le nom d'unions taguées ou de types de données algébriques).

Que sont les Unions Discrétisées ?

Une union discrétisée est un type qui représente une valeur pouvant être l'un de plusieurs types différents. Chacun de ces types, connus sous le nom de membres de l'union, partage une propriété commune et distincte appelée le discriminant ou la balise. Ce discriminant permet à TypeScript de déterminer précisément quel membre de l'union est actuellement actif, permettant un contrôle de type puissant et une auto-complétion.

Pensez-y comme à un feu de circulation. Il peut être dans l'un des trois états : Rouge, Jaune ou Vert. La propriété 'couleur' agit comme le discriminant, nous indiquant exactement dans quel état se trouve le feu.

Pourquoi Utiliser les Unions Discrétisées pour les Machines d'État ?

Les unions discrétisées apportent plusieurs avantages clés lors de la construction de machines d'état en TypeScript :

Définir une Machine d'État avec des Unions Discrétisées

Illustrons comment définir une machine d'état en utilisant des unions discrétisées avec un exemple pratique : un système de traitement de commandes. Une commande peut être dans les états suivants : En attente, En traitement, Expédiée, et Livrée.

Étape 1 : Définir les Types d'État

Tout d'abord, nous définissons les types individuels pour chaque état. Chaque type aura une propriété `type` agissant comme le discriminant, ainsi que toutes les données spécifiques à l'état.


interface Pending {
  type: "pending";
  orderId: string;
  customerName: string;
  items: string[];
}

interface Processing {
  type: "processing";
  orderId: string;
  assignedAgent: string;
}

interface Shipped {
  type: "shipped";
  orderId: string;
  trackingNumber: string;
}

interface Delivered {
  type: "delivered";
  orderId: string;
  deliveryDate: Date;
}

Étape 2 : Créer le Type d'Union Discrétisée

Ensuite, nous créons l'union discrétisée en combinant ces types individuels en utilisant l'opérateur `|` (union).


type OrderState = Pending | Processing | Shipped | Delivered;

Maintenant, `OrderState` représente une valeur qui peut être soit `Pending`, `Processing`, `Shipped`, ou `Delivered`. La propriété `type` à l'intérieur de chaque état agit comme le discriminant, permettant à TypeScript de les différencier.

Gérer les Transitions d'État

Maintenant que nous avons défini notre machine d'état, nous avons besoin d'un mécanisme pour passer d'un état à l'autre. Créons une fonction `processOrder` qui prend l'état actuel et une action en entrée et retourne le nouvel état.


interface Action {
  type: string;
  payload?: any;
}

function processOrder(state: OrderState, action: Action): OrderState {
  switch (state.type) {
    case "pending":
      if (action.type === "startProcessing") {
        return {
          type: "processing",
          orderId: state.orderId,
          assignedAgent: action.payload.agentId,
        };
      }
      return state; // Pas de changement d'état

    case "processing":
      if (action.type === "shipOrder") {
        return {
          type: "shipped",
          orderId: state.orderId,
          trackingNumber: action.payload.trackingNumber,
        };
      }
      return state; // Pas de changement d'état

    case "shipped":
      if (action.type === "deliverOrder") {
        return {
          type: "delivered",
          orderId: state.orderId,
          deliveryDate: new Date(),
        };
      }
      return state; // Pas de changement d'état

    case "delivered":
      // La commande est déjà livrée, pas d'autres actions
      return state;

    default:
      // Ceci ne devrait jamais arriver grâce à la vérification d'exhaustivité
      return state; // Ou lancer une erreur
  }
}

Explication

Exploiter la Vérification d'Exhaustivité

La vérification d'exhaustivité de TypeScript est une fonctionnalité puissante qui garantit que vous gérez tous les états possibles de votre machine d'état. Si vous ajoutez un nouvel état à l'union `OrderState` mais oubliez de mettre à jour la fonction `processOrder`, TypeScript signalera une erreur.

Pour activer la vérification d'exhaustivité, vous pouvez utiliser le type `never`. À l'intérieur du cas `default` de votre instruction switch, assignez l'état à une variable de type `never`.


function processOrder(state: OrderState, action: Action): OrderState {
  switch (state.type) {
    // ... (cas précédents) ...

    default:
      const _exhaustiveCheck: never = state;
      return _exhaustiveCheck; // Ou lancer une erreur
  }
}

Si l'instruction `switch` gère toutes les valeurs possibles de `OrderState`, la variable `_exhaustiveCheck` sera de type `never` et le code compilera. Cependant, si vous ajoutez un nouvel état à l'union `OrderState` et oubliez de le gérer dans l'instruction `switch`, la variable `_exhaustiveCheck` sera d'un type différent, et TypeScript générera une erreur à la compilation, vous alertant du cas manquant.

Exemples Pratiques et Applications

Les unions discrétisées sont applicables dans un large éventail de scénarios au-delà du simple traitement de commandes :

Exemple : Gestion de l'État de l'UI

Considérons un exemple simple de gestion de l'état d'un composant UI qui récupère des données d'une API. Nous pouvons définir les états suivants :


interface Initial {
  type: "initial";
}

interface Loading {
  type: "loading";
}

interface Success {
  type: "success";
  data: T;
}

interface Error {
  type: "error";
  message: string;
}

type UIState = Initial | Loading | Success | Error;

function renderUI(state: UIState): React.ReactNode {
  switch (state.type) {
    case "initial":
      return 

Cliquez sur le bouton pour charger les données.

; case "loading": return

Chargement...

; case "success": return
{JSON.stringify(state.data, null, 2)}
; case "error": return

Erreur : {state.message}

; default: const _exhaustiveCheck: never = state; return _exhaustiveCheck; } }

Cet exemple démontre comment les unions discrétisées peuvent être utilisées pour gérer efficacement les différents états d'un composant UI, en garantissant que l'UI est correctement rendue en fonction de l'état actuel. La fonction `renderUI` gère chaque état de manière appropriée, offrant un moyen clair et type-safe de gérer l'UI.

Bonnes Pratiques pour Utiliser les Unions Discrétisées

Pour utiliser efficacement les unions discrétisées dans vos projets TypeScript, considérez les bonnes pratiques suivantes :

Techniques Avancées

Types Conditionnels

Les types conditionnels peuvent être combinés avec des unions discrétisées pour créer des machines d'état encore plus puissantes et flexibles. Par exemple, vous pouvez utiliser des types conditionnels pour définir différents types de retour pour une fonction en fonction de l'état actuel.


function getData(state: UIState): T | undefined {
  if (state.type === "success") {
    return state.data;
  }
  return undefined;
}

Cette fonction utilise une simple instruction `if` mais pourrait être rendue plus robuste en utilisant des types conditionnels pour garantir qu'un type spécifique est toujours retourné.

Types Utilitaires

Les types utilitaires de TypeScript, tels que `Extract` et `Omit`, peuvent être utiles lors du travail avec des unions discrétisées. `Extract` vous permet d'extraire des membres spécifiques d'un type union basé sur une condition, tandis que `Omit` vous permet de supprimer des propriétés d'un type.


// Extraire l'état "success" de l'union UIState
type SuccessState = Extract, { type: "success" }>;

// Omettre la propriété 'message' de l'interface Error
type ErrorWithoutMessage = Omit;

Exemples Concrets dans Différentes Industries

La puissance des unions discrétisées s'étend à travers divers secteurs et domaines d'application :

Conclusion

Les unions discrétisées TypeScript offrent un moyen puissant et type-safe de construire des machines d'état. En définissant clairement les états et les transitions possibles, vous pouvez créer un code plus robuste, maintenable et compréhensible. La combinaison de la sécurité de type, de la vérification d'exhaustivité et de l'auto-complétion améliorée fait des unions discrétisées un outil inestimable pour tout développeur TypeScript traitant de la gestion d'état complexe. Adoptez les unions discrétisées dans votre prochain projet et découvrez les avantages de la gestion d'état type-safe. Comme nous l'avons montré avec des exemples diversifiés allant de l'e-commerce à la santé, en passant par la logistique et l'éducation, le principe de la gestion d'état type-safe par le biais d'unions discrétisées est universellement applicable.

Que vous construisiez un composant UI simple ou une application d'entreprise complexe, les unions discrétisées peuvent vous aider à gérer l'état plus efficacement et à réduire le risque d'erreurs d'exécution. Alors, plongez et explorez le monde des machines d'état type-safe avec TypeScript !