Types de guérilla dans TypeScript
Design de police rebelle
Types de guérilla en TypeScript
Dans cet article, nous explorerons trois techniques (intéressantes, voire désastreuses ?) pour aider à la conception de types !
L’objectif principal est d’obtenir des interfaces de modèle/entité/classe cohérentes et prévisibles.
- Approches pour la conception des types
- Technique #1 : Pourquoi pas tout ?
- Technique #2 : Mix-ins
- Technique #3 : Organisation avec des espaces de noms
- Résumé
Approches pour la conception des types
Vous avez probablement rencontré ou rédigé divers modèles concernant les « implémentations de types ». Surtout lorsqu’il s’agit de consommer des données provenant d’API tierces.
Remarque : Je mets délibérément de côté les processus « traditionnels » comme la création de diagrammes Entité-Relation (ERD) ou les hiérarchies d’héritage orienté objet (OOP). Ici, nous créons des types pour représenter des données d’API semi-structurées.
Explorons deux approches de haut niveau : Objet unique et volumineux (Top-down) vs. Types nommés multiples (Bottom-up).
Objet unique et volumineux
Privilégie l’explicitisme par rapport à la réutilisabilité et à l’absence de redondance (DRY).
Avantage : L’expérience IDE/éditeur est excellente, car les infobulles affichent un aperçu plus complet – sans complication.
interface ProductDetails { name: string; seller: { name: string }; availability: Array<{ warehouseId: string; quantity: number }>; reviews: Array<{ authorId: number; stars: number }>;}Puisque nous privilégions l’explicitisme, il est acceptable d’accepter une certaine répétition (dans des limites raisonnables). Quand des groupes de propriétés se répètent fréquemment, n’hésitez pas à extraire ces champs répétés vers un type nommé.
Types nommés multiples
Privilégie la réutilisabilité et l’absence de répétition.
Cette approche est probablement la plus adoptée.
interface ProductDetails { name: string; seller: Seller; reviews: Reviews[]; availability: Availability[];}interface Seller { name: string; }interface Availability { warehouseId: string; quantity: number; }interface Reviews { authorId: number; stars: number; }Dans l’ensemble, cette approche est excellente. Mais elle n’est pas sans inconvénients.
- Lisibilité excellente au départ ; cependant, elle peut se dégrader à mesure que la taille et le nombre de types augmentent.
- Extrêmement DRY, mais au prix de quel coût ? (Plus de détails plus loin.)
- L’expérience développeur peut souffrir car les infobulles sont moins informatives.
⚠️ Depuis (environ) la version 3 de TypeScript, le Serveur de langage tronque les infobulles, omettant les propriétés imbriquées.
💡 Il existe des astuces pour améliorer un peu les choses. Essayez de maintenir appuyé surCmd ou Ctrl, puis de passer le curseur sur différents noms de types – vous devriez voir au moins une couche supplémentaire de propriétés dans l’infobulle.
Pourquoi devons-nous choisir entre ces deux approches ? (Grand type vs. sous-types nommés.)
Technique #1 : Pourquoi pas tout ?
Pouvons-nous tout avoir ?
- Clarté des types « à vue d’ensemble » ?
- Plus de sous-types nommés ?
- Sans duplication ?
✅ OUI ! 🎉
export interface ProductDetails { name: string; seller: { name: string }; reviews: Array<{ authorId: number; stars: number }>; availability: Array<{ warehouseId: string; quantity: number }>;}export type Seller = ProductDetails["seller"];export type Review = ProductDetails["reviews"][number];export type Availability = ProductDetails["availability"][number];- Créer des types structurés « principaux » de grande taille.
- Exporter des sous-types dérivés du type principal.
Cette approche se révèle particulièrement efficace dans les systèmes où les objets « haut niveau » bénéficient d’une documentation centralisée. De plus, cette technique favorise la réutilisation entre de nombreux cas d’utilisation : Modèles, Services, Résultats de requêtes, etc.
Technique #2 : Mix-ins
Cette stratégie consiste à regrouper les bons champs, avec les bons noms, pour représenter des objets logiques uniques. L’objectif est d’aborder efficacement plusieurs cas d’utilisation en utilisant les utilitaires TypeScript et les unions de types.
Cette approche diffère de l’héritage et des hiérarchies traditionnels de l’OOA, qui visent à créer des couches d’objets organisées en taxonomies étroitement liées. L’approche mix-in repose sur des types plats et faiblement liés, regroupant des champs associés tout en réduisant les redondances.
Exemples de mix-in
interface TodoModel { text: string; complete: boolean;}interface InstanceMixin { id: number;}/** TodoDraft représente l'état du formulaire, tous les champs pouvant être indéfinis */export type TodoDraft = Partial<TodoModel>;/** Todo représente un enregistrement d'instance Todo depuis la base de données */export type Todo = TodoModel & InstanceMixin;Exemple User
interface User { id: number; name: string; bio: string; social: Record<"facebook" | "instagram" | "github", URL>;}Représentons l’User avant et après l’enregistrement dans la base de données.
// Champs de base de l'utilisateur (par exemple pour un <form>)interface UserBase { name: string; bio: string; social: Record<"facebook" | "instagram" | "github", URL>;}// Champs provenant de la base de donnéesinterface InstanceMixin { id: number; createdAt: Date; updatedAt: Date;}// Un **instance** User - avec tous les champstype UserInstance = InstanceMixin & UserBase;Maintenant, nous pouvons définir précisément les champs requis (comme password pour la création/mise à jour, mais non inclus dans les requêtes de UserInstance).
interface UserBase { name: string; bio: string; social: Record<"facebook" | "instagram" | "github", URL>;}interface InstanceMixin { id: number; createdAt: Date; updatedAt: Date;}/** Type de données utilisateur pour l'inscription, incluant le champ `password` */export type UserPayload = UserBase & { password: string };/** Représente le type User renvoyé par le serveur. */export type UserInstance = UserBase & InstanceMixin;- “Est-ce une bonne pratique ?”
- “Devrais-je l’essayer ?”
Aucune idée. Continuons !
Technique #3 : Organisation avec les espaces de noms
Ici, nous déclarons un espace de noms ModelMixins. Cela apporte une certaine organisation ainsi qu’un schéma de réutilisation plus clair.
Formes standardisées
createdAtetupdatedAtexistent ensemble.id, et nonIDou_id.
// `src/types/mixins.d.ts`namespace ModelMixins { interface Identity { id: number; } interface Timestamp { createdAt: Date; updatedAt: Date; } type Instance = ModelMixins.Identity & ModelMixins.Timestamp; interface HashedPassword { passwordHash: string; } interface InputPassword { password: string; }}Utilisation de l’Union de types
// `src/types/user.d.ts`export interface UserBase { name: string; bio: string; social: Record<"facebook" | "instagram" | "github", URL>;}// Type unique `User`, utilisant l'Union de types pour représenter// dynamiquement les états avant et après création.export type User = | (UserBase & ModelMixins.Instance & ModelMixins.HashedPassword) | (UserBase & ModelMixins.InputPassword);Si souhaité, vous pouvez également exporter des types nommés individuellement :
/** Données utilisateur pour l'inscription, incluant le champ `password` */export type UserPayload = UserBase & ModelMixins.Instance & ModelMixins.HashedPassword;/** Représente le type User renvoyé par le serveur. */export type UserInstance = UserBase & ModelMixins.InputPassword;Utilisation dans le monde réel
Voici une fonction upsert() qui utilise l’opérateur in pour distinguer les types UserInstance et UserPayload.
function upsert(user: User) { if ("id" in user) { // TypeScript sait que `user` ici possède les champs de Instance (id, createdAt, etc) return updateUser(user.id, user); } else { // TypeScript sait que cette version de user est `UserBase & ModelMixins.InputPassword` return createUser(user); }}Résumé
Nous avons couvert trois techniques et quelques idées connexes.
Vous vous demandez peut-être, s’agit-il de bonnes pratiques ? Devriez-vous adopter certaines de ces idées ?
Ressources
- Conseils TypeScript pour les projets hérités : N’utilisez que les types dont vous avez besoin
- L’excellente nouvelle livre de Matt Pocock
- Conseils Total TypeScript