Explorez les puissantes alternatives aux enums TypeScript telles que les assertions `const` et les types union. Comprenez leurs avantages, inconvénients et applications pratiques pour un code plus propre et maintenable dans un contexte de développement mondial.
Alternatives aux Enums TypeScript : Naviguer entre les Assertions const et les Types Union pour un Code Robuste
TypeScript, un puissant sur-ensemble de JavaScript, apporte le typage statique au monde dynamique du développement web. Parmi ses nombreuses fonctionnalités, le mot-clé enum a longtemps été une solution de choix pour définir un ensemble de constantes nommées. Les enums fournissent un moyen clair de représenter une collection fixe de valeurs liées, améliorant la lisibilité et la sûreté des types.
Cependant, à mesure que l'écosystème TypeScript mûrit et que les projets gagnent en complexité et en envergure, les développeurs du monde entier remettent de plus en plus en question l'utilité traditionnelle des enums. Bien que simples pour des cas basiques, les enums introduisent certains comportements et caractéristiques à l'exécution qui peuvent parfois entraîner des problèmes inattendus, impacter la taille du bundle ou compliquer les optimisations de tree-shaking. Cela a conduit à une exploration généralisée d'alternatives.
Ce guide complet explore en profondeur deux alternatives importantes et très efficaces aux enums TypeScript : les Types Union avec des littéraux de chaîne/numériques et les Assertions Const (as const). Nous examinerons leurs mécanismes, leurs applications pratiques, leurs avantages et leurs compromis, vous fournissant les connaissances nécessaires pour prendre des décisions de conception éclairées pour vos projets, quelle que soit leur taille ou l'équipe mondiale qui y travaille. Notre objectif est de vous permettre d'écrire un code TypeScript plus robuste, maintenable et efficace.
L'Enum TypeScript : Un Bref Rappel
Avant de nous plonger dans les alternatives, revisitons brièvement l'enum traditionnel de TypeScript. Les enums permettent aux développeurs de définir un ensemble de constantes nommées, rendant le code plus lisible et empêchant les "chaînes magiques" ou les "nombres magiques" d'être dispersés dans une application. Ils se présentent sous deux formes principales : les enums numériques et les enums de chaînes de caractères.
Enums Numériques
Par défaut, les enums TypeScript sont numériques. Le premier membre est initialisé avec 0, et chaque membre suivant est auto-incrémenté.
enum Direction {
Up,
Down,
Left,
Right,
}
let currentDirection: Direction = Direction.Up;
console.log(currentDirection); // Affiche : 0
console.log(Direction.Left); // Affiche : 2
Vous pouvez également initialiser manuellement les membres d'un enum numérique :
enum StatusCode {
Success = 200,
NotFound = 404,
ServerError = 500,
}
let status: StatusCode = StatusCode.NotFound;
console.log(status); // Affiche : 404
Une caractéristique particulière des enums numériques est le mapping inversé. À l'exécution, un enum numérique est compilé en un objet JavaScript qui mappe à la fois les noms aux valeurs et les valeurs aux noms.
enum UserRole {
Admin = 1,
Editor,
Viewer,
}
console.log(UserRole[1]); // Affiche : "Admin"
console.log(UserRole.Editor); // Affiche : 2
console.log(UserRole[2]); // Affiche : "Editor"
/*
Compile en JavaScript :
var UserRole;
(function (UserRole) {
UserRole[UserRole["Admin"] = 1] = "Admin";
UserRole[UserRole["Editor"] = 2] = "Editor";
UserRole[UserRole["Viewer"] = 3] = "Viewer";
})(UserRole || (UserRole = {}));
*/
Enums de Chaînes de Caractères
Les enums de chaînes de caractères sont souvent préférés pour leur lisibilité à l'exécution, car ils ne reposent pas sur des nombres auto-incrémentés. Chaque membre doit être initialisé avec un littéral de chaîne.
enum UserPermission {
Read = "READ_PERMISSION",
Write = "WRITE_PERMISSION",
Delete = "DELETE_PERMISSION",
}
let permission: UserPermission = UserPermission.Write;
console.log(permission); // Affiche : "WRITE_PERMISSION"
Les enums de chaînes de caractères n'obtiennent pas de mapping inversé, ce qui est généralement une bonne chose pour éviter un comportement inattendu à l'exécution et réduire la sortie JavaScript générée.
Considérations Clés et Pièges Potentiels des Enums
Bien que les enums offrent de la commodité, ils présentent certaines caractéristiques qui méritent une attention particulière :
- Objets à l'exécution : Les enums numériques et de chaînes de caractères génèrent des objets JavaScript à l'exécution. Cela signifie qu'ils contribuent à la taille du bundle de votre application, même si vous ne les utilisez que pour la vérification de type. Pour les petits projets, cela peut être négligeable, mais dans les applications à grande échelle avec de nombreux enums, cela peut s'accumuler.
- Absence de Tree-Shaking : Parce que les enums sont des objets à l'exécution, ils ne sont souvent pas "tree-shakés" efficacement par les bundlers modernes comme Webpack ou Rollup. Si vous définissez un enum mais n'utilisez qu'un ou deux de ses membres, l'objet enum entier pourrait tout de même être inclus dans votre bundle final. Cela peut conduire à des tailles de fichiers plus importantes que nécessaire.
- Mapping Inversé (Enums Numériques) : La fonctionnalité de mapping inversé des enums numériques, bien que parfois utile, peut aussi être une source de confusion et de comportement inattendu. Elle ajoute du code supplémentaire à la sortie JavaScript et pourrait ne pas toujours être la fonctionnalité souhaitée. Par exemple, la sérialisation des enums numériques peut parfois conduire à ce que seul le nombre soit stocké, ce qui peut ne pas être aussi descriptif qu'une chaîne de caractères.
- Surcharge de Transpilation : La compilation des enums en objets JavaScript ajoute une légère surcharge au processus de build par rapport à la simple définition de variables constantes.
- Itération Limitée : Itérer directement sur les valeurs d'un enum peut être non trivial, en particulier avec les enums numériques en raison du mapping inversé. Vous avez souvent besoin de fonctions d'aide ou de boucles spécifiques pour obtenir uniquement les valeurs souhaitées.
Ces points soulignent pourquoi de nombreuses équipes de développement mondiales, en particulier celles axées sur la performance et la taille du bundle, se tournent vers des alternatives qui offrent une sûreté de type similaire sans l'empreinte à l'exécution ou d'autres complexités.
Alternative 1 : Les Types Union avec des Littéraux
L'une des alternatives les plus simples et les plus puissantes aux enums en TypeScript est l'utilisation des Types Union avec des Littéraux de Chaîne ou Numériques. Cette approche tire parti du système de types robuste de TypeScript pour définir un ensemble de valeurs spécifiques et autorisées à la compilation, sans introduire de nouvelles constructions à l'exécution.
Que sont les Types Union ?
Un type union décrit une valeur qui peut être de plusieurs types. Par exemple, string | number signifie qu'une variable peut contenir soit une chaîne de caractères, soit un nombre. Lorsqu'il est combiné avec des types littéraux (par exemple, "success", 404), vous pouvez définir un type qui ne peut contenir qu'un ensemble spécifique de valeurs prédéfinies.
Exemple Pratique : Définir des Statuts avec les Types Union
Considérons un scénario courant : définir un ensemble de statuts possibles pour une tâche de traitement de données ou le compte d'un utilisateur. Avec les types union, cela semble propre et concis :
type JobStatus = "PENDING" | "IN_PROGRESS" | "COMPLETED" | "FAILED";
function processJob(status: JobStatus): void {
if (status === "COMPLETED") {
console.log("Tâche terminée avec succès.");
} else if (status === "FAILED") {
console.log("La tâche a rencontré une erreur.");
} else {
console.log(`La tâche est actuellement ${status}.`);
}
}
let currentJobStatus: JobStatus = "IN_PROGRESS";
processJob(currentJobStatus);
// Ceci entraînerait une erreur de compilation :
// let invalidStatus: JobStatus = "CANCELLED"; // Erreur : Le type '"CANCELLED"' n'est pas assignable au type 'JobStatus'.
Pour les valeurs numériques, le modèle est identique :
type HttpCode = 200 | 400 | 404 | 500;
function handleResponse(code: HttpCode): void {
if (code === 200) {
console.log("Opération réussie.");
} else if (code === 404) {
console.log("Ressource non trouvée.");
}
}
let responseStatus: HttpCode = 200;
handleResponse(responseStatus);
Remarquez comment nous définissons un alias de type ici. C'est une construction purement de compilation. Une fois compilé en JavaScript, JobStatus disparaît simplement, et les chaînes/nombres littéraux sont utilisés directement.
Avantages des Types Union avec des Littéraux
Cette approche offre plusieurs avantages convaincants :
- Purement à la compilation : Les types union sont entièrement effacés lors de la compilation. Ils ne génèrent aucun code JavaScript à l'exécution, ce qui se traduit par des tailles de bundle plus petites et des temps de démarrage d'application plus rapides. C'est un avantage significatif pour les applications critiques en termes de performance et celles déployées mondialement où chaque kilooctet compte.
- Excellente sûreté des types : TypeScript vérifie rigoureusement les assignations par rapport aux types littéraux définis, offrant de solides garanties que seules les valeurs valides sont utilisées. Cela prévient les bugs courants associés aux fautes de frappe ou aux valeurs incorrectes.
- Tree-Shaking optimal : Comme il n'y a pas d'objet à l'exécution, les types union supportent intrinsèquement le tree-shaking. Votre bundler n'inclut que les littéraux de chaîne ou numériques que vous utilisez réellement, pas un objet entier.
- Lisibilité : Pour un ensemble fixe de valeurs simples et distinctes, la définition du type est souvent très claire et facile à comprendre.
- Simplicité : Aucune nouvelle construction de langage ou artefact de compilation complexe n'est introduit. Il s'agit simplement de tirer parti des fonctionnalités de base des types de TypeScript.
- Accès direct aux valeurs : Vous travaillez directement avec les valeurs de chaîne ou de nombre, ce qui simplifie la sérialisation et la désérialisation, en particulier lors de l'interaction avec des API ou des bases de données qui attendent des identifiants de chaîne spécifiques.
Inconvénients des Types Union avec des Littéraux
Bien que puissants, les types union ont aussi quelques limitations :
- Répétition pour les données associées : Si vous devez associer des données ou métadonnées supplémentaires à chaque membre de l'"enum" (par exemple, une étiquette d'affichage, une icône, une couleur), vous ne pouvez pas le faire directement dans la définition du type union. Vous auriez généralement besoin d'un objet de mappage distinct.
- Pas d'itération directe sur toutes les valeurs : Il n'y a pas de moyen intégré d'obtenir un tableau de toutes les valeurs possibles à partir d'un type union à l'exécution. Par exemple, vous ne pouvez pas facilement obtenir
["PENDING", "IN_PROGRESS", "COMPLETED", "FAILED"]directement à partir deJobStatus. Cela nécessite souvent de maintenir un tableau séparé de valeurs si vous avez besoin de les afficher dans une interface utilisateur (par exemple, un menu déroulant). - Moins centralisé : Si l'ensemble de valeurs est nécessaire à la fois comme type et comme tableau de valeurs à l'exécution, vous pourriez vous retrouver à définir la liste deux fois (une fois comme type, une fois comme tableau à l'exécution), ce qui peut introduire un potentiel de désynchronisation.
Malgré ces inconvénients, pour de nombreux scénarios, les types union fournissent une solution propre, performante et sûre en termes de types qui s'aligne bien avec les pratiques de développement JavaScript modernes.
Alternative 2 : Les Assertions Const (as const)
L'assertion as const, introduite dans TypeScript 3.4, est un autre outil incroyablement puissant qui offre une excellente alternative aux enums, surtout lorsque vous avez besoin d'un objet à l'exécution et d'une inférence de type robuste. Elle permet à TypeScript d'inférer le type le plus étroit possible pour les expressions littérales.
Que sont les Assertions Const ?
Lorsque vous appliquez as const à une variable, un tableau ou un objet littéral, TypeScript traite toutes les propriétés de ce littéral comme readonly et infère leurs types littéraux au lieu de types plus larges (par exemple, "foo" au lieu de string, 123 au lieu de number). Cela permet de dériver des types union très spécifiques à partir de structures de données à l'exécution.
Exemple Pratique : Créer un objet "Pseudo-Enum" avec as const
Revenons à notre exemple de statut de tâche. Avec as const, nous pouvons définir une source unique de vérité pour nos statuts, qui agit à la fois comme un objet à l'exécution et comme base pour les définitions de type.
const JobStatuses = {
PENDING: "PENDING",
IN_PROGRESS: "IN_PROGRESS",
COMPLETED: "COMPLETED",
FAILED: "FAILED",
} as const;
// JobStatuses.PENDING est maintenant inféré comme type "PENDING" (pas seulement string)
// JobStatuses est inféré comme type {
// readonly PENDING: "PENDING";
// readonly IN_PROGRESS: "IN_PROGRESS";
// readonly COMPLETED: "COMPLETED";
// readonly FAILED: "FAILED";
// }
À ce stade, JobStatuses est un objet JavaScript à l'exécution, tout comme un enum classique. Cependant, son inférence de type est beaucoup plus précise.
Combinaison avec typeof et keyof pour les Types Union
La véritable puissance émerge lorsque nous combinons as const avec les opérateurs typeof et keyof de TypeScript pour dériver un type union à partir des valeurs ou des clés de l'objet.
const JobStatuses = {
PENDING: "PENDING",
IN_PROGRESS: "IN_PROGRESS",
COMPLETED: "COMPLETED",
FAILED: "FAILED",
} as const;
// Type représentant les clés (par ex., "PENDING" | "IN_PROGRESS" | ...)
type JobStatusKeys = keyof typeof JobStatuses;
// Type représentant les valeurs (par ex., "PENDING" | "IN_PROGRESS" | ...)
type JobStatusValues = typeof JobStatuses[keyof typeof JobStatuses];
function processJobWithConstAssertion(status: JobStatusValues): void {
if (status === JobStatuses.COMPLETED) {
console.log("Tâche terminée avec succès.");
} else if (status === JobStatuses.FAILED) {
console.log("La tâche a rencontré une erreur.");
} else {
console.log(`La tâche est actuellement ${status}.`);
}
}
let currentJobStatusFromObject: JobStatusValues = JobStatuses.IN_PROGRESS;
processJobWithConstAssertion(currentJobStatusFromObject);
// Ceci entraînerait une erreur de compilation :
// let invalidStatusFromObject: JobStatusValues = "CANCELLED"; // Erreur !
Ce modèle offre le meilleur des deux mondes : un objet à l'exécution pour l'itération ou l'accès direct aux propriétés, et un type union à la compilation pour une vérification de type stricte.
Avantages des Assertions Const avec des Types Union dérivés
- Source unique de vérité : Vous définissez vos constantes une seule fois dans un objet JavaScript simple, et en dérivez à la fois l'accès à l'exécution et les types à la compilation. Cela réduit considérablement la duplication et améliore la maintenabilité au sein d'équipes de développement diverses.
- Sûreté des types : Similaire aux types union purs, vous obtenez une excellente sûreté de type, garantissant que seules les valeurs prédéfinies sont utilisées.
- Itérabilité à l'exécution : Puisque
JobStatusesest un objet JavaScript simple, vous pouvez facilement itérer sur ses clés ou valeurs en utilisant des méthodes JavaScript standard commeObject.keys(),Object.values(), ouObject.entries(). C'est inestimable pour les interfaces utilisateur dynamiques (par exemple, peupler des menus déroulants) ou la journalisation. - Données associées : Ce modèle supporte naturellement l'association de données supplémentaires à chaque membre de l'"enum".
- Meilleur potentiel de Tree-Shaking (par rapport aux Enums) : Bien que
as constcrée un objet à l'exécution, c'est un objet JavaScript standard. Les bundlers modernes sont généralement plus efficaces pour éliminer les propriétés non utilisées ou même des objets entiers s'ils ne sont pas référencés, par rapport à la sortie de compilation des enums de TypeScript. Cependant, si l'objet est volumineux et que seules quelques propriétés sont utilisées, l'objet entier pourrait tout de même être inclus s'il est importé d'une manière qui empêche un tree-shaking granulaire. - Flexibilité : Vous pouvez définir des valeurs qui ne sont pas seulement des chaînes ou des nombres, mais aussi des objets plus complexes si nécessaire, ce qui en fait un modèle très flexible.
const FileOperations = {
UPLOAD: {
label: "Téléverser un fichier",
icon: "upload-icon.svg",
permission: "can_upload"
},
DOWNLOAD: {
label: "Télécharger un fichier",
icon: "download-icon.svg",
permission: "can_download"
},
DELETE: {
label: "Supprimer un fichier",
icon: "delete-icon.svg",
permission: "can_delete"
},
} as const;
type FileOperationType = keyof typeof FileOperations; // "UPLOAD" | "DOWNLOAD" | "DELETE"
type FileOperationDetail = typeof FileOperations[keyof typeof FileOperations]; // { label: string; icon: string; permission: string; }
function performOperation(opType: FileOperationType) {
const details = FileOperations[opType];
console.log(`Exécution de : ${details.label} (Permission : ${details.permission})`);
}
performOperation("UPLOAD");
Inconvénients des Assertions Const
- Présence d'un objet à l'exécution : Contrairement aux types union purs, cette approche crée toujours un objet JavaScript à l'exécution. Bien qu'il s'agisse d'un objet standard et souvent meilleur pour le tree-shaking que les enums, il n'est pas entièrement effacé.
- Définition de type légèrement plus verbeuse : La dérivation du type union (
keyof typeof ...outypeof ...[keyof typeof ...]) nécessite un peu plus de syntaxe que la simple énumération de littéraux pour un type union. - Potentiel de mauvaise utilisation : S'il n'est pas utilisé avec précaution, un très grand objet
as constpourrait encore contribuer de manière significative à la taille du bundle si son contenu n'est pas efficacement "tree-shaké" à travers les frontières des modules.
Pour les scénarios où vous avez besoin à la fois d'une vérification de type robuste à la compilation et d'une collection de valeurs à l'exécution qui peut être itérée ou fournir des données associées, as const est souvent le choix préféré des développeurs TypeScript du monde entier.
Comparaison des Alternatives : Quand Utiliser Quoi ?
Le choix entre les types union et les assertions const dépend en grande partie de vos besoins spécifiques concernant la présence à l'exécution, l'itérabilité et la nécessité d'associer des données supplémentaires à vos constantes. Analysons les facteurs de décision.
Simplicité vs Robustesse
- Types Union : Offrent la simplicité ultime lorsque vous avez seulement besoin d'un ensemble de valeurs de chaîne ou numériques distinctes et sûres au niveau des types à la compilation. C'est l'option la plus légère.
- Assertions Const : Fournissent un modèle plus robuste lorsque vous avez besoin à la fois de la sûreté des types à la compilation et d'un objet à l'exécution qui peut être interrogé, itéré ou étendu avec des métadonnées supplémentaires. La configuration initiale est légèrement plus verbeuse, mais elle est payante en termes de fonctionnalités.
Présence à l'Exécution vs à la Compilation
- Types Union : Sont des constructions purement de compilation. Ils ne génèrent absolument aucun code JavaScript. C'est idéal pour les applications où la minimisation de la taille du bundle est primordiale, et où les valeurs elles-mêmes sont suffisantes sans avoir besoin d'y accéder en tant qu'objet à l'exécution.
- Assertions Const : Génèrent un objet JavaScript simple à l'exécution. Cet objet est accessible et utilisable dans votre code JavaScript. Bien qu'il ajoute à la taille du bundle, il est généralement plus efficace que les enums TypeScript et constitue un meilleur candidat pour le tree-shaking.
Besoins d'Itérabilité
- Types Union : N'offrent pas de moyen direct d'itérer sur toutes les valeurs possibles à l'exécution. Si vous devez remplir un menu déroulant ou afficher toutes les options, vous devrez définir un tableau séparé de ces valeurs, ce qui peut entraîner une duplication.
- Assertions Const : Excellent dans ce domaine. Comme vous travaillez avec un objet JavaScript standard, vous pouvez facilement utiliser
Object.keys(),Object.values(), ouObject.entries()pour obtenir un tableau des clés, des valeurs, ou des paires clé-valeur, respectivement. Cela les rend parfaits pour les interfaces utilisateur dynamiques ou tout scénario nécessitant une énumération à l'exécution.
const PaymentMethods = {
CREDIT_CARD: "Carte de Crédit",
PAYPAL: "PayPal",
BANK_TRANSFER: "Virement Bancaire",
} as const;
type PaymentMethodType = keyof typeof PaymentMethods;
// Obtenir toutes les clés (par ex., pour la logique interne)
const methodKeys = Object.keys(PaymentMethods) as PaymentMethodType[];
console.log(methodKeys); // ["CREDIT_CARD", "PAYPAL", "BANK_TRANSFER"]
// Obtenir toutes les valeurs (par ex., pour l'affichage dans un menu déroulant)
const methodLabels = Object.values(PaymentMethods);
console.log(methodLabels); // ["Carte de Crédit", "PayPal", "Virement Bancaire"]
// Obtenir les paires clé-valeur (par ex., pour le mappage)
const methodEntries = Object.entries(PaymentMethods);
console.log(methodEntries); // [["CREDIT_CARD", "Carte de Crédit"], ...]
Implications sur le Tree-Shaking
- Types Union : Sont intrinsèquement "tree-shakables" car ils n'existent qu'à la compilation.
- Assertions Const : Bien qu'elles créent un objet à l'exécution, les bundlers modernes peuvent souvent éliminer les propriétés non utilisées de cet objet plus efficacement qu'avec les objets enum générés par TypeScript. Cependant, si l'objet entier est importé et référencé, il sera probablement inclus. Une conception minutieuse des modules peut aider.
Meilleures Pratiques et Approches Hybrides
Ce n'est pas toujours une situation de "l'un ou l'autre". Souvent, la meilleure solution implique une approche hybride, en particulier dans les grandes applications internationalisées :
- Pour des indicateurs ou identifiants simples, purement internes, qui n'ont jamais besoin d'être itérés ou d'avoir des données associées, les Types Union sont généralement le choix le plus performant et le plus propre.
- Pour les ensembles de constantes qui doivent être itérées, affichées dans des interfaces utilisateur ou avoir des métadonnées riches associées (comme des étiquettes, des icônes ou des permissions), le modèle des Assertions Const est supérieur.
- Combinaison pour la lisibilité et la localisation : De nombreuses équipes utilisent
as constpour les identifiants internes, puis dérivent les étiquettes d'affichage localisées à partir d'un système d'internationalisation (i18n) distinct.
// src/constants/order-status.ts
const OrderStatuses = {
PENDING: "PENDING",
PROCESSING: "PROCESSING",
SHIPPED: "SHIPPED",
DELIVERED: "DELIVERED",
CANCELLED: "CANCELLED",
} as const;
type OrderStatus = typeof OrderStatuses[keyof typeof OrderStatuses];
export { OrderStatuses, type OrderStatus };
// src/i18n/fr.json
{
"orderStatus": {
"PENDING": "En attente de confirmation",
"PROCESSING": "En cours de traitement",
"SHIPPED": "Expédiée",
"DELIVERED": "Livrée",
"CANCELLED": "Annulée"
}
}
// src/components/OrderStatusDisplay.tsx
import { OrderStatuses, type OrderStatus } from "../constants/order-status";
import { useTranslation } from "react-i18next"; // Exemple de bibliothèque i18n
interface OrderStatusDisplayProps {
status: OrderStatus;
}
function OrderStatusDisplay({ status }: OrderStatusDisplayProps) {
const { t } = useTranslation();
const displayLabel = t(`orderStatus.${status}`);
return <span>Statut : {displayLabel}</span>;
}
// Utilisation :
// <OrderStatusDisplay status={OrderStatuses.DELIVERED} />
Cette approche hybride tire parti de la sûreté de type et de l'itérabilité à l'exécution de as const tout en gardant les chaînes d'affichage localisées séparées et gérables, une considération essentielle pour les applications mondiales.
Modèles Avancés et Considérations
Au-delà de l'utilisation de base, les types union et les assertions const peuvent être intégrés dans des modèles plus sophistiqués pour améliorer davantage la qualité et la maintenabilité du code.
Utilisation des Gardes de Type (Type Guards) avec les Types Union
Lorsque vous travaillez avec des types union, en particulier lorsque l'union inclut des types variés (pas seulement des littéraux), les gardes de type deviennent essentiels pour affiner les types. Avec les types union littéraux, les unions discriminées offrent une puissance immense.
type SuccessEvent = { type: "SUCCESS"; data: any; };
type ErrorEvent = { type: "ERROR"; message: string; code: number; };
type SystemEvent = SuccessEvent | ErrorEvent;
function handleSystemEvent(event: SystemEvent) {
if (event.type === "SUCCESS") {
console.log("Données reçues :", event.data);
// event est maintenant affiné en SuccessEvent
} else {
console.log("Erreur survenue :", event.message, "Code :", event.code);
// event est maintenant affiné en ErrorEvent
}
}
handleSystemEvent({ type: "SUCCESS", data: { user: "Alice" } });
handleSystemEvent({ type: "ERROR", message: "Échec du réseau", code: 503 });
Ce modèle, souvent appelé "unions discriminées", est incroyablement robuste et sûr en termes de types, offrant des garanties à la compilation sur la structure de vos données basées sur une propriété littérale commune (le discriminateur).
Object.values() avec as const et les Assertions de Type
Lors de l'utilisation du modèle as const, Object.values() peut être très utile. Cependant, l'inférence par défaut de TypeScript pour Object.values() peut être plus large que souhaité (par exemple, string[] au lieu d'une union spécifique de littéraux). Vous pourriez avoir besoin d'une assertion de type pour plus de rigueur.
const Statuses = {
ACTIVE: "Actif",
INACTIVE: "Inactif",
PENDING: "En attente",
} as const;
type StatusValue = typeof Statuses[keyof typeof Statuses]; // "Actif" | "Inactif" | "En attente"
// Object.values(Statuses) est inféré comme (string | "Actif" | "Inactif" | "En attente")[]
// Nous pouvons l'asserter plus étroitement si nécessaire :
const allStatusValues: StatusValue[] = Object.values(Statuses);
console.log(allStatusValues); // ["Actif", "Inactif", "En attente"]
// Pour un menu déroulant, vous pouvez associer les valeurs aux étiquettes si elles diffèrent
const statusOptions = Object.entries(Statuses).map(([key, value]) => ({
value: key, // Utiliser la clé comme identifiant réel
label: value // Utiliser la valeur comme étiquette d'affichage
}));
console.log(statusOptions);
/*
[
{ value: "ACTIVE", label: "Actif" },
{ value: "INACTIVE", label: "Inactif" },
{ value: "PENDING", label: "En attente" }
]
*/
Ceci démontre comment obtenir un tableau de valeurs fortement typé, adapté aux éléments d'interface utilisateur tout en conservant les types littéraux.
Internationalisation (i18n) et Étiquettes Localisées
Pour les applications mondiales, la gestion des chaînes localisées est primordiale. Alors que les enums TypeScript et leurs alternatives fournissent des identifiants internes, les étiquettes d'affichage doivent souvent être séparées pour l'i18n. Le modèle as const complète magnifiquement les systèmes i18n.
Vous définissez vos identifiants internes et immuables en utilisant as const. Ces identifiants sont cohérents dans toutes les langues et servent de clés pour vos fichiers de traduction. Les chaînes d'affichage réelles sont ensuite récupérées à partir d'une bibliothèque i18n (par exemple, react-i18next, vue-i18n, FormatJS) en fonction de la langue sélectionnée par l'utilisateur.
// app/features/product/constants.ts
export const ProductCategories = {
ELECTRONICS: "ELECTRONICS",
APPAREL: "APPAREL",
HOME_GOODS: "HOME_GOODS",
BOOKS: "BOOKS",
} as const;
export type ProductCategory = typeof ProductCategories[keyof typeof ProductCategories];
// app/i18n/locales/en.json
{
"productCategories": {
"ELECTRONICS": "Electronics",
"APPAREL": "Apparel & Accessories",
"HOME_GOODS": "Home Goods",
"BOOKS": "Books"
}
}
// app/i18n/locales/fr.json
{
"productCategories": {
"ELECTRONICS": "Électronique",
"APPAREL": "VĂŞtements et Accessoires",
"HOME_GOODS": "Articles pour la maison",
"BOOKS": "Livres"
}
}
// app/components/ProductCategorySelector.tsx
import { ProductCategories, type ProductCategory } from "../features/product/constants";
import { useTranslation } from "react-i18next";
function ProductCategorySelector() {
const { t } = useTranslation();
return (
<select>
{Object.values(ProductCategories).map(categoryKey => (
<option key={categoryKey} value={categoryKey}>
{t(`productCategories.${categoryKey}`)}
</option>
))}
</select>
);
}
Cette séparation des préoccupations est cruciale pour les applications mondiales et évolutives. Les types TypeScript garantissent que vous utilisez toujours des clés valides, et le système i18n gère la couche de présentation en fonction de la langue de l'utilisateur. Cela évite d'avoir des chaînes dépendantes de la langue directement intégrées dans la logique de votre application principale, un anti-modèle courant pour les équipes internationales.
Conclusion : Renforcer vos Choix de Conception en TypeScript
Alors que TypeScript continue d'évoluer et de permettre aux développeurs du monde entier de créer des applications plus robustes et évolutives, il devient de plus en plus important de comprendre ses fonctionnalités nuancées et ses alternatives. Bien que le mot-clé enum de TypeScript offre un moyen pratique de définir des constantes nommées, son empreinte à l'exécution, ses limitations en matière de tree-shaking et les complexités du mapping inversé rendent souvent les alternatives modernes plus attrayantes pour les projets sensibles à la performance ou à grande échelle.
Les Types Union avec des Littéraux de Chaîne/Numériques se distinguent comme la solution la plus légère et la plus centrée sur la compilation. Ils offrent une sûreté de type sans compromis sans générer de JavaScript à l'exécution, ce qui les rend idéaux pour les scénarios où une taille de bundle minimale et un tree-shaking maximal sont des priorités, et où l'énumération à l'exécution n'est pas une préoccupation.
D'un autre côté, les Assertions Const (as const) combinées avec typeof et keyof offrent un modèle très flexible et puissant. Elles fournissent une source unique de vérité pour vos constantes, une forte sûreté de type à la compilation et la capacité essentielle d'itérer sur les valeurs à l'exécution. Cette approche est particulièrement bien adaptée aux situations où vous devez associer des données supplémentaires à vos constantes, peupler des interfaces utilisateur dynamiques ou vous intégrer de manière transparente avec des systèmes d'internationalisation.
En examinant attentivement les compromis – empreinte à l'exécution, besoins d'itérabilité et complexité des données associées – vous pouvez prendre des décisions éclairées qui mènent à un code TypeScript plus propre, plus efficace et plus maintenable. Adopter ces alternatives ne consiste pas seulement à écrire du TypeScript "moderne" ; il s'agit de faire des choix architecturaux délibérés qui améliorent la performance de votre application, l'expérience des développeurs et sa durabilité à long terme pour un public mondial.
Renforcez votre développement TypeScript en choisissant le bon outil pour la bonne tâche, en allant au-delà de l'enum par défaut lorsque de meilleures alternatives existent.