Accueil / Articles

Ce que les enums en C, Java, Rust, Go et TypeScript disent sur la conception des types

Comparez la manière dont sept langages définissent les enums, des entiers nommés aux unions étiquetées et aux objets en temps de exécution, et découvrez ce que cela implique pour la modélisation en TypeScript.

Éditeur : REACTAPP.TOP17 min de lecture3847 mots

Sources: Enums Were Never Just Constants

Presque toutes les langages dominants disposent d’une notion appelée enum, et presque chacun d’eux lui donne un sens différent. Dans une langue, il s’agit d’une étiquette simple associée à un entier ; dans une autre, c’est une classe dotée de méthodes ; dans une troisième, c’est une union de données de formes différentes ; et en TypeScript, c’est l’une des rares constructions au niveau du type qui va au-delà de JavaScript pur. En examinant C, C++, Java, Rust, Go, TypeScript et Odin côte à côte, ces différences deviennent concrètes, ce qui vous permet d’avoir un vocabulaire plus précis pour décider quel type utiliser dans votre propre code TypeScript lorsque une valeur ne peut être qu’une des quelques possibilités.

Un seul mot, plusieurs problèmes distincts

L’image classique est simple : on énumère un petit nombre de noms, et chacun de ces noms représente l’une des valeurs autorisées :

Red
Green
Blue

Cela semble être un concept universel, mais dès que l’on compare les implémentations, il se fragmente :

  • C traite un enum principalement comme un groupe nommé de constantes entières.
  • C++ conserve ce modèle mais ajoute enum class, qui introduit un encadrement de portée et une typisation plus stricte.
  • Java fait de chaque constante enum un véritable objet d’une classe enum, doté de champs, de constructeurs, de méthodes et même de corps de classe spécifiques à chaque constante.
  • Rust permet à un seul type enum de contenir des variantes qui manipulent des données entièrement différentes.
  • Go ne dispose pas de mot-clé enum ; il propose plutôt des constantes et l’identifiant iota.
  • Les enums de TypeScript sont compilés en objets JavaScript qui existent à l’exécution.
  • Odin garde les enums délibérément simples et confie les tâches connexes à d’autres fonctionnalités, telles que des ensembles de bits et des tableaux indexés par enum.

La raison pour laquelle il n’existe pas de définition unique est que « enum » représente en réalité un ensemble de problématiques qui se retrouvent par hasard à partager un mot-clé. Le concepteur d’un langage doit décider dans quelle mesure cette structure doit assumer les fonctions suivantes :

  • attribuer des noms à un ensemble fixe de valeurs
  • créer un type distinct plutôt que de réutiliser un type existant
  • lier des données à des valeurs individuelles
  • représenter plusieurs états possibles d’une valeur
  • permettre au compilateur de prouver que tous les cas ont été traités
  • représenter de manière compacte des combinaisons de flags
  • décider ce qui, le cas échéant, sera conservé dans le programme en exécution

Chaque langage choisit un sous-ensemble différent, et ces choix révèlent beaucoup sur ses priorités.

C : noms lisible pour les entiers

L’enum traditionnel en C est un outil pratique pour nommer des constantes entières. Un type de statut peut être déclaré de cette manière :

enum Status {
    STATUS_OK,
    STATUS_ERROR,
    STATUS_PENDING
};

En réalité, ces trois noms correspondent simplement à des entiers consécutifs commençant par zéro :

0
1
2

Cela peut sembler anodin, mais cela résout un véritable problème de lisibilité. Imaginez une comparaison avec un simple nombre caché au sein d’une logique de configuration :

if (status == 2) {
    ...
}

Rien dans cette ligne n’indique à un examinateur ce que représente 2. La même vérification écrite avec une constante nommée s’accompagne d’une documentation explicite :

if (status == STATUS_PENDING) {
    ...
}

C’est la justification la plus ancienne des enums : ils remplacent les nombres magiques par des noms qui transmettent une intention. Ils s’accordent également naturellement avec switch, où chaque cas peut être interprété comme une affirmation liée au domaine concerné plutôt qu’à des opérations arithmétiques :

switch (status) {
    case STATUS_OK:
        ...
        break;

Les autres cas suivent le même schéma :

    case STATUS_ERROR:
        ...
        break;    case STATUS_PENDING:
        ...
        break;
}

Puisque les valeurs sont des entiers ordinaires et prévisibles, les enums en C s’intègrent parfaitement dans le code de bas niveau : ils correspondent aux registres matériels, aux champs de protocole et aux formats de fichiers sans aucune surprise.

L’inconvénient réside dans un contrôle faible. Le C ne traite pas un enum comme un ensemble fermé, contrairement à Rust. Les constantes d’énumération se comportent comme des entiers, une variable enum peut généralement recevoir n’importe quel entier sans problème, et le compilateur n’a aucune obligation de signaler l’absence d’un case. De nombreux compilateurs émettent des avertissements pour les valeurs d’énumération non gérées dans un switch, mais il s’agit là d’une fonctionnalité offerte par l’outil plutôt que d’une garantie linguistique. Le C23 a ajouté une syntaxe permettant de déclarer un type sous-jacent fixe, ce qui comble une lacune que C++ avait déjà résolue des années auparavant.

La philosophie du C peut se résumer ainsi :

Donnez aux entiers de meilleurs noms et limitez au strict minimum les mécanismes qui les entourent.

Pour la programmation système, ce minimalisme est souvent exactement ce que l’on souhaite.

C++ : la même idée avec des limites plus strictes

C++ a hérité des enums de style C et a ensuite introduit une forme plus stricte, l’énumération scopée :

enum class Status {
    Ok,
    Error,
    Pending
};

Ce changement n’est pas simplement une astuce syntaxique. Les énumérateurs d’une énumération scopée doivent être qualifiés par le nom du type :

Status::Ok

au lieu d’apparaître comme un identifiant simple dans le scope environnant :

Ok

Autant dire que, tout aussi important, une énumération scopée ne se convertit pas implicitement en entier. Vous ne pouvez pas comparer par erreur un Status avec un Color non lié, ni le passer là où un int est attendu, sans une conversion explicite, ce qui élimine toute une catégorie de confusions silencieuses.

C++ vous permet également de choisir explicitement le type de stockage :

enum class Status : uint8_t {
    Ok,
    Error,
    Pending
};

Cela est important chaque fois que le layout fait partie du contrat, par exemple dans les paquets réseau ou les structures partagées avec du code C. Le langage distingue les enums non scellés de ceux scellés déclarés avec enum class ou enum struct, et les deux formes acceptent un type de base fixe.

Le schéma à retenir est que C++ ne redéfinit pas ce qu’est un enum. Il ajoute un contrôle sur trois aspects liés à celui-ci :

  • la manière dont les noms sont scellés
  • le degré de séparation stricte du type par rapport aux entiers
  • la manière dont la valeur est représentée en mémoire

Au fond, il s’agit toujours d’une liste de valeurs nommées ; le programmeur peut simplement définir des limites plus strictes autour d’elle.

Java : les enums en tant que partie du modèle objet

Java s’éloigne encore davantage des entiers. La déclaration a l’air familière :

enum Status {
    OK,
    ERROR,
    PENDING
}

Cependant, OK, ERROR et PENDING ne sont pas des alias numériques. Selon la Spécification du langage Java, un enum est une forme spéciale et contrainte de classe dont les instances sont toutes déclarées à l’avance par nom. Chaque constante est un objet, créé une seule fois lors de l’initialisation de la classe, et étant donné qu’il s’agit d’un objet, il peut conserver un état et présenter un comportement.

Constantes qui contiennent des données

Un argument de constructeur peut associer une valeur à chaque constante. Ici, chaque planète enregistre sa masse :

enum Planet {
    EARTH(5.97),
    MARS(0.642);

Le champ ainsi que le constructeur qui le remplit complètent la déclaration :

    final double mass;    Planet(double mass) {
        this.mass = mass;
    }
}

Chaque constante dispose désormais de ses propres données, et le code qui a besoin de la masse d’une planète lit directement un champ au lieu de consulter une table de référence distincte qui pourrait devenir désynchronisée.

Constantes qui présentent un comportement

Les enums peuvent également déclarer des méthodes, et chaque constante peut fournir sa propre implémentation. Le type d’opération suivant surcharge une méthode apply abstraite pour chaque constante :

enum Operation {
    ADD {
        @Override
        int apply(int a, int b) {
            return a + b;
        }
    },

La deuxième constante et la déclaration abstraite se présentent comme ceci :

    MULTIPLY {
        @Override
        int apply(int a, int b) {
            return a * b;
        }
    };    abstract int apply(int a, int b);
}

À ce stade, l’enum est clairement une classe, et c’est intentionnel. Java intègre les enums dans son modèle orienté objet, de sorte qu’une constante peut posséder des champs, des arguments de constructeur, des méthodes partagées ainsi que des surcharges spécifiques à la constante. Une constante disposant de son propre corps de classe est compilée en tant que sous-classe anonyme du type enum, et dans les versions récentes de Java, la spécification décrit une telle classe enum comme étant implicitement scellée, puisque l’ensemble des sous-classes est défini par la déclaration.

Ainsi, la position de Java est approximativement :

Si une valeur provient d’un ensemble fixe, pourquoi la limiter à un entier alors qu’elle pourrait être un objet ayant son propre comportement ?

C’est pourquoi les enums de Java conviennent parfaitement aux machines à états, aux analyseurs syntaxiques, aux tables d’opérateurs avec priorité, ainsi qu’aux mécanismes de dispatch de type stratégie où chaque option sait comment effectuer son propre travail.

Lorsque la structure des données dépend de la variante

Les enums de Java se révèlent utiles lorsque chaque constante contient le même type d’informations prédéfinies. Un problème différent apparaît lorsque chaque alternative nécessite une structure différente.

Prenons par exemple les messages dans un protocole de chat :

TextMessage
ImageMessage
VideoMessage

Un message de texte contient une chaîne, un message d’image contient des octets d’image ainsi que des métadonnées, et un message vidéo contient à nouveau quelque chose d’autre. Ce qui varie ici n’est pas la valeur d’un champ, mais plutôt la structure des données elles-mêmes. Une énumération de style Java, où chaque constante partage un même ensemble de champs, s’y prête mal. C’est précisément le problème pour lequel les énumérations de Rust ont été conçues.

Rust : les énumérations en tant qu’unions étiquetées

Au Rust, chaque variant d’une énumération peut posséder son propre chargement de données :

enum Message {
    Quit,
    Move { x: i32, y: i32 },
    Write(String),
    ChangeColor(u8, u8, u8),
}

Quit ne transporte rien, Move possède des champs nommés, Write enveloppe une String, et ChangeColor contient trois octets. Pour créer une valeur, on choisit un variant et on lui fournit ses données :

let message = Message::Move { x: 10, y: 20 };

Un autre variant du même type se présente comme ceci :

let message = Message::Write(String::from("Hello"));

Les deux expressions produisent un Message, mais les informations qu’elles contiennent ont une structure complètement différente. Cette construction est ce que la théorie des types appelle un type somme, et ce que les programmeurs de systèmes appellent souvent une union étiquetée : une étiquette indique quelle variante est active, tandis que le chargement de données contient les informations correspondant à cette variante. Le type décrit donc un ensemble fermé d’alternatives :

Message
 ├── Quit
 ├── Move { x, y }
 ├── Write(String)
 └── ChangeColor(r, g, b)

Comparaison exhaustive

Le compilateur connaît chaque variante, et l’instruction match utilise ces informations. Le premier bras gère le cas Quit :

match message {
    Message::Quit => {
        println!("Quit");
    }

Les bras restants déstructurent le chargement de données de chaque variante et assignent ses champs à des noms locaux :

    Message::Move { x, y } => {
        println!("Move to {}, {}", x, y);
    }    Message::Write(text) => {
        println!("{}", text);
    }    Message::ChangeColor(r, g, b) => {
        println!("RGB: {}, {}, {}", r, g, b);
    }
}

Si vous supprimez l’un de ces membres, le programme ne se compile plus ; Rust rejette un match non exhaustif. La documentation de Rust présente les enums et la correspondance de motifs ensemble pour cette raison précise. Ajouter une nouvelle variante ultérieurement transforme chaque match incomplet dans le code en une erreur de compilation qui indique les endroits nécessitant une mise à jour.

C’est là la différence essentielle par rapport à un enum en C. Un enum en C répond :

À quelle constante nommée ce nombre correspond-il ?

Un enum en Rust répond :

Quelle variante est active actuellement, et quel chargement l’accompagne-t-il ?

La deuxième question couvre un éventail bien plus large de conceptions.

Rendre les états invalides irreprésentables

Les avantages pratiques sont les plus évidents à travers les résultats. Une opération qui peut réussir ou échouer peut être modélisée comme un type possédant deux variantes :

Result
 ├── Success(value)
 └── Error(error)

Une valeur qui peut être présente ou non est traitée de la même manière :

Option
 ├── Some(value)
 └── None

La bibliothèque standard de Rust propose ces deux concepts sous la forme de Result et Option, qui ne sont en réalité que des enums ordinaires. Comparez cela à l’approche basée sur des champs séparés, courante dans de nombreux projets :

value = null
hasValue = false
error = ...

Avec des champs distincts, rien n’empêche que hasValue soit true tandis que value est nul, ou qu’une erreur et une valeur soient mises à jour en même temps. Chaque utilisateur doit se souvenir des règles non écrites. Avec un type somme, de telles combinaisons ne peuvent tout simplement pas être créées, car la seule façon de construire une valeur est via l’une des variantes déclarées.

Cela résume l’un des objectifs les plus fondamentaux de la conception des enums :

Un enum peut encoder des règles concernant la structure de vos données directement dans le système de types.

Lorsque ces règles font partie du type, le compilateur peut détecter les cas manquants avant même que quoi que ce soit ne s’exécute. Les développeurs TypeScript disposent d’un équivalent proche grâce aux unions discriminées, où un champ littéral commun comme kind remplit la fonction de tag, tandis qu’un contrôle never assure l’exhaustivité.

Go : pas de mot-clé enum du tout

Après Rust, l’approche de Go peut sembler un pas en arrière. Il n’y a pas de mot-clé enum. L’idiome consiste en un bloc de constantes générées par iota :

const (
    StatusOK = iota
    StatusError
    StatusPending
)

Dans une déclaration de constante, iota renvoie l’index de la spécification de constante en cours, commençant à zéro, et il peut apparaître à l’intérieur d’expressions. Lorsqu’une ligne du bloc omet son expression, Go répète la précédente, ce qui se produit dans le cas suivant :

const (
    A = iota
    B
    C
)

ce qui donne :

A = 0
B = 1
C = 2

Puisque la répétition implicite copie toute l’expression, iota peut servir à plus que simplement compter. Un décalage par son biais génère des puissances de deux :

const (
    Read  = 1 << iota
    Write
    Execute
)

Les valeurs résultantes sont :

Read    = 1
Write   = 2
Execute = 4

Des bits distincts font de ces constantes des flags naturels : Read | Write combine les permissions, et un ET binaire vérifie l’existence d’une d’entre elles. Vous pouvez également introduire un type nommé afin que les constantes ne soient pas simplement des entiers non structurés :

type Permission uint8

puis déclarer les constantes avec ce type :

const (
    Read Permission = 1 << iota
    Write
    Execute
)

Seul Read décrit explicitement Permission, mais comme l’expression est répétée, le type est transmis, ce qui fait que Write et Execute sont également des valeurs de Permission. La spécification Go distingue les constantes typées des constantes non typées, et un type nommé offre une certaine protection contre le mélange de valeurs non liées. Cela ne crée cependant pas le type d’identité d’énumération présent dans les langages disposant d’une construction dédiée.

C’est là une caractéristique de Go : plutôt que d’ajouter une fonctionnalité, il propose un système de constantes flexible et laisse l’abstraction de haut niveau au programmeur. Le prix à payer est que le langage ne lie jamais formellement

these constants

à

this closed set of variants

comme le font Rust ou Java. Toute valeur du type sous-jacent peut être convertie en Permission, et le compilateur ne vérifie pas que les cas d’un switch sur ces constantes soient exhaustifs. Les équipes qui souhaitent cette garantie ajoutent généralement un outil de linting ou un générateur de code en complément.

TypeScript : une enum qui existe à l’exécution

TypeScript est principalement une couche de types statiques par-dessus JavaScript. Un alias de type comme celui-ci :

type User = {
    name: string;
};

ne laisse aucune trace dans le JavaScript généré ; il est effacé lors de la compilation. Les enums enfreignent cette règle :

enum Direction {
    Up,
    Down,
    Left,
    Right
}

Une enum ordinaire de TypeScript se compile en un véritable objet JavaScript, et le manuel de TypeScript décrit explicitement ce comportement à l’exécution. Pour les enums numériques, l’objet généré comprend des mappages tant directs que inverses, ce qui permet de passer d’un nom à un nombre et inversement.

Direction.Up    -> 0
Direction[0]    -> "Up"

Cela surprend les développeurs qui pensent que tout ce que TypeScript ajoute n’a d’effet qu’en temps de compilation. Cela a également des conséquences pratiques : l’enum ajoute du code à votre bundle, et itérer sur un enum numérique avec Object.keys renvoie les clés numériques sous forme de chaînes de caractères aux côtés des noms.

Les enums de type chaîne attribuent des valeurs de chaîne explicites :

enum Direction {
    Up = "UP",
    Down = "DOWN",
    Left = "LEFT",
    Right = "RIGHT"
}

Ils ne disposent pas de mappages inverses, et leurs valeurs sont beaucoup plus faciles à lire dans les journaux, les en-têtes de réseau et les sorties des débogueurs, car "UP" s’explique de lui-même tandis que 0 ne le fait pas. TypeScript prend également en charge les membres calculés et const enum, qui est intégré directement au point d’utilisation et supprimé de la sortie. Gardez à l’esprit que const enum interagit mal avec les outils qui compilent un fichier à la fois, c’est pourquoi de nombreux projets l’évitent.

L’alternative objet-plus-comme-const

La documentation de TypeScript montre également un schéma plus simple composé d’un objet ordinaire et d’une assertion as const :

const Direction = {
    Up: 0,
    Down: 1,
    Left: 2,
    Right: 3,
} as const;

Ce code est un JavaScript classique que n’importe quel outil comprend, tandis que as const attribue des types littéraux à TypeScript pour chaque propriété. Un type d’union correspondant peut être dérivé avec (typeof Direction)[keyof typeof Direction]. Ce schéma est devenu encore plus pertinent puisque les environnements d’exécution ont acquis la capacité de supprimer directement les types : cette approche ne retire que les annotations, et les enums, qui nécessitent une véritable génération de code, ne s’y prêtent pas. Consultez la documentation actuelle de votre environnement d’exécution et de votre compilateur pour connaître les règles exactes ; l’article sur ce que le support natif de TypeScript dans Node.js fait réellement aborde ce sujet en détail.

Le point fondamental est que la conception des enums en TypeScript est influencée par son environnement d’exécution. Java pourrait définir des enums en tant que fonctionnalité de premier ordre dès le début. TypeScript doit coexister avec JavaScript, qui ne dispose pas de enums propres. La question se pose donc ainsi :

En quoi une fonctionnalité au niveau des types doit-elle se transformer lorsque l’environnement d’exécution est JavaScript ?

Il n’y a pas de réponse claire, c’est pourquoi TypeScript a fini par adopter plusieurs options qui se chevauchent.

Odin : des enums simples accompagnées d’outils orthogonaux

Odin emprunte une autre approche. Ses enums restent proches de l’idée classique d’un ensemble nommé de valeurs, et au lieu d’étendre l’enum en une hiérarchie d’objets ou un type de données algébrique, le langage propose des mécanismes distincts pour les problèmes similaires : des types différents, des tableaux énumérés indexés par un enum, des unions, ainsi que des ensembles de bits dont le type d’élément peut être un enum.

Direction :: enum {
    North,
    East,
    South,
    West,
}

Un ensemble de bits basé sur cet enum représente alors n’importe quelle combinaison de directions :

Direction_Set :: bit_set[Direction]

Le principe de conception repose sur une division du travail :

  • un ensemble fixe de valeurs constitue un enum
  • une collection de ces valeurs forme un ensemble de bits
  • les données associées à chaque valeur se trouvent dans un tableau indexé par l’enum
  • les alternatives présentant des structures de données différentes constituent une union

Puisque chaque outil a une fonction précise, on peut l’analyser indépendamment, sans qu’il doive assumer la charge de tous les autres. Comparez cela à Go, où les flags sont créés manuellement à l’aide de opérations de décalage, ou à Rust, où l’enum lui-même couvre à la fois des listes simples et des unions étiquetées.

Aucun langage n’a parfaitement résolu le problème des enums

Il est tentant de classer ces approches, mais elles répondent à des questions différentes. Une comparaison concise :

  • C : constantes entières nommées, optimisées pour la simplicité, la lisibilité et la compatibilité de bas niveau.
  • C++ : même base avec un scoping optionnel, une typisation plus stricte et un type sous-jacent explicite.
  • Java : un ensemble fixe d’instances d’une classe enum, capable de contenir des champs, des constructeurs, des méthodes et un comportement spécifique à chaque constante.
  • Rust : un ensemble fermé de variantes, chacune capable de contenir ses propres données, avec une correspondance de motifs exhaustive imposée par le compilateur.
  • Go : pas de constructeur dédié ; les constantes et iota génèrent des valeurs séquentielles ou calculées.
  • TypeScript : des enums qui existent sous forme d’objets JavaScript en temps de exécution, contrairement à la plupart des fonctionnalités de types de TypeScript, ainsi que des alternatives effaçables.
  • Odin : des enums simples complétées par des ensembles de bits, des tableaux énumérés et des unions pour des tâches similaires.
  • Aucun de ces systèmes n’est une version améliorée du précédent. Chacun reflète ce que ses concepteurs voulaient que cette fonctionnalité gère.

    Ce que la conception des enums révèle sur un langage

    Les enums sont suffisamment simples pour être apprises en une après-midi, tout en révélant la philosophie du langage qui les entoure. Elles s’inscrivent dans la question que chaque langage semble se poser :

    • C : comment peut-on représenter cela efficacement à l’aide de primitives simples ?
    • C++ : comment conserver un contrôle proche du matériel tout en ajoutant des abstractions plus sûres ?
    • Java : que se passerait-il si chaque élément d’un ensemble fermé était un objet complet avec ses propres méthodes ?
    • Rust : un seul type peut-il décrire tous les états légaux ainsi que le contenu associé à chacun ?
    • Go : est-il vraiment nécessaire de disposer d’une fonctionnalité dédiée ?
    • TypeScript : en devenant du JavaScript pur, quelle devrait être la fonctionnalité liée au temps de compilation ?
    • Odin : le concept de base peut-il rester simple tandis que des fonctionnalités orthogonales gèrent les besoins connexes ?

    Puisque ces langages sont optimisés pour des objectifs différents, les enums n’ont jamais abouti à une seule conception unique.

    Les enums représentent des décisions de modélisation, pas de la syntaxe

    Il est facile de considérer un enum comme une partie de la syntaxe :

    enum {
        A,
        B,
        C
    }
    

    La syntaxe est la partie la moins intéressante. La vraie question est de savoir ce que vous souhaitez que le système de types garantisse concernant les valeurs que vous avez listées :

    A, B, C
    

    Travailler sur cette question signifie répondre à plusieurs questions plus petites :

    • S’agit-il simplement de noms pour des nombres ?
    • Chaque valeur a-t-elle besoin de données associées ?
    • Chaque variante a-t-elle besoin d’un comportement propre ?
    • Le compilateur doit-il savoir que ce sont les seuls états valides et en assurer le traitement exhaustif ?
    • Avez-vous besoin de flags binaires compacts ?
    • Les valeurs doivent-elles exister en temps de exécution, pour l’itération ou la validation ?
    • Comment doivent-elles être serialisées pour être transmises ou stockées ?

    Diverses réponses mènent à des conceptions différentes, et les conséquences vont bien au-delà de la syntaxe : l’organisation de la mémoire, la sécurité des types, ce que le compilateur peut analyser, les coûts en temps de exécution, la correspondance de motifs, la structure de l’API, le format de sérialisation, et en fin de compte la façon dont l’équipe perçoit ses données.

    Parfois, l’enum approprié n’est pas un enum

    Cela a des conséquences directes sur l’utilisation quotidienne de TypeScript. Un langage propose un enum parce qu’un problème particulier existe, mais cela ne signifie pas que tout ensemble fixe de valeurs doive en devenir un. Selon la situation :

    • une union de littéraux de chaîne est souvent plus claire et ne coûte rien en temps de exécution
    • un objet simple avec as const permet d’obtenir des valeurs et des types en temps de exécution sans compilation spéciale
    • une classe dédiée est pertinente lorsque chaque option implique un comportement important
    • un ensemble de bits ou un entier à flags est l’outil idéal pour les combinaisons
  • Une union discriminée est ce dont vous avez réellement besoin lorsque les variantes contiennent des données différentes.
  • Parfois, trois constantes bien nommées suffisent pour structurer le problème comme il se doit.
  • Une bonne conception de types part des données et de leurs invariants, et non de la clé mot que le langage propose par hasard.

    Points clés

    • « Enum » regroupe plusieurs aspects indépendants : la nomenclature, le typage distinct, les données associées, les formes des variantes, l’épuisement des cas possibles, les flags et la présence en temps de exécution.
    • C et C++ se concentrent sur les entiers et leur représentation ; Java se concentre sur les objets ; Rust se concentre sur les types somme avec une vérification par le compilateur.
    • Go et Odin illustrent deux approches opposées pour éviter un enum polyvalent : le construire soi-même à partir de constantes, ou diviser la tâche entre des fonctionnalités spécialisées.
    • Les enums de TypeScript sont des objets réels en temps d’exécution, ce qui a un impact sur les bundles, l’itération et la compatibilité avec les outils de suppression des types ; les unions littérales et les objets as const sont souvent le choix le plus simple.
    • Au lieu de recourir à une enum, déterminez quelles garanties vous nécessitez. Le construct que vous choisissez doit découler de ces garanties, et non l’inverse.