


Comment utiliser TypeScript pour accumuler des types : saisir TOUS les résultats fetch() possibles
Quand j'ai commencé à réécrire (avec mon équipe) notre application en TypeScript et Svelte (c'était en JavaScript et React qu'on déteste tous), j'ai été confronté à un problème :
Comment puis-je saisir en toute sécurité tous les corps possibles d'une réponse HTTP ?
Ça vous dit quelque chose ? Sinon, vous faites probablement partie de ceux-là, hehe. Faisons une parenthèse un instant pour mieux comprendre l’image.
Pourquoi ce domaine semble inexploré
Personne ne semble se soucier de « tous les corps possibles » d'une réponse HTTP, car je n'ai rien trouvé de déjà fait pour cela (enfin, peut-être ts-fetch). Permettez-moi de passer rapidement en revue ma logique ici pour expliquer pourquoi.
Personne ne s'en soucie parce que les gens non plus :
Ne vous souciez que du chemin heureux : le corps de la réponse lorsque le code d'état HTTP est 2xx.
Les gens le saisissent manuellement ailleurs.
Pour le n°1, je dirais que oui, les développeurs (surtout les inexpérimentés) oublient qu'une requête HTTP peut échouer et que les informations contenues dans la réponse échouée sont très probablement complètement différentes de la réponse normale.
Pour le n°2, examinons un gros problème rencontré dans les packages NPM populaires comme ky et axios.
Le problème avec les packages de récupération de données
Pour autant que je sache, les gens aiment les packages comme ky ou axios parce que l'une de leurs « fonctionnalités » est qu'ils génèrent une erreur sur les codes d'état HTTP non OK. Depuis quand est-ce que ça va ? Depuis jamais. Mais apparemment, les gens ne s’en rendent pas compte. Les gens sont heureux et contents de recevoir des erreurs sur les réponses non OK.
J'imagine que les gens tapent des corps non OK quand il est temps d'attraper. Quel gâchis, quelle odeur de code !
Il s'agit d'une odeur de code car vous utilisez effectivement des blocs try..catch comme instructions de branchement, et try..catch n'est pas censé être une instruction de branchement.
Mais même si vous disiez avec moi que le branchement se produit naturellement dans try..catch, il y a une autre raison importante pour laquelle cela reste mauvais : lorsqu'une erreur est générée, le runtime doit dérouler la pile d'appels. C'est beaucoup plus coûteux en termes de cycles CPU qu'un branchement classique avec une instruction if ou switch.
Sachant cela, pouvez-vous justifier la baisse de performances simplement pour abuser du bloc try..catch ? Je dis non. Je ne vois pas une seule raison pour laquelle le monde du front-end semble être parfaitement satisfait de cela.
Maintenant que j’ai expliqué mon raisonnement, revenons au sujet principal.
Le problème, détaillé
Une réponse HTTP peut contenir des informations différentes en fonction de son code d'état. Par exemple, un point de terminaison de tâche tel que api/todos/:id qui reçoit une requête HTTP PATCH peut renvoyer une réponse avec un corps différent lorsque le code d'état de la réponse est 200 que lorsque le code d'état de la réponse est 400.
Prenons un exemple :
// For the 200 response, a copy of the updated object: { "id": 123, "text": "The updated text" } // For the 400 response, a list of validation errors: { "errors": [ "The updated text exceeds the maximum allowed number of characters." ] }
Donc, en gardant cela à l'esprit, nous revenons à l'énoncé du problème : comment puis-je taper une fonction qui effectue cette requête PATCH où TypeScript peut me dire à quel corps je travaille, en fonction du code d'état HTTP au moment où j'écris code? La réponse : utilisez une syntaxe fluide (syntaxe du constructeur, syntaxe chaînée) pour accumuler des types.
Construire la solution
Commençons par définir un type qui s'appuie sur un type précédent :
export type AccumType<T, NewT> = T | NewT;
Super simple : étant donné les types T et NewT, joignez-les pour former un nouveau type. Utilisez à nouveau ce nouveau type comme T dans AccumType<>, et vous pourrez alors accumuler un autre nouveau type. Cependant, cela fait à la main n’est pas agréable. Présentons un autre élément clé de la solution : la syntaxe fluide.
Syntaxe courante
Étant donné un objet de classe X dont les méthodes renvoient toujours lui-même (ou une copie de lui-même), on peut enchaîner les appels de méthodes les uns après les autres. Il s'agit d'une syntaxe fluide, ou d'une syntaxe chaînée.
Écrivons une classe simple qui fait ceci :
export class NamePending<T> { accumulate<NewT>() { return this as NamePending<AccumType<T, NewT>>; } } // Now you can use it like this: const x = new NamePending<{ a: number; }>(); // x is of type NamePending<{ a: number; }>. const y = x.accumulate<{ b: string; }> // y is of type NamePending<{ a: number; } | { b: string; }>.
Euréka ! Nous avons réussi à combiner la syntaxe fluide et le type que nous avons écrit pour commencer à accumuler des types de données en un seul type !
Au cas où cela ne serait pas évident, vous pouvez continuer l'exercice jusqu'à ce que vous ayez accumulé les types souhaités (x.accumulate().accumulate()… jusqu'à ce que vous ayez terminé).
Tout cela est bien beau, mais ce type super simple ne lie pas le code d'état HTTP au type de corps correspondant.
Affiner ce que nous avons
Ce que nous voulons, c'est fournir à TypeScript suffisamment d'informations pour que sa fonctionnalité de restriction de type entre en jeu. Pour ce faire, faisons le nécessaire pour obtenir du code pertinent pour le problème d'origine (en tapant les corps des réponses HTTP dans un format par défaut). -base de code de statut).
Tout d’abord, renommez et faites évoluer AccumType. Le code ci-dessous montre la progression en itérations :
// Iteration 1. export type FetchResult<T, NewT> = T | NewT; // Iteration 2. export type FetchResponse<TStatus extends number, TBody> = { ok: boolean; status: TStatus; statusText: string; body: TBody }; export type FetchResult<T, TStatus extends number, NewT> = T | FetchResponse<TStatus, NewT>; //Makes sense to rename NewT to TBody.
À ce stade, j'ai réalisé quelque chose : les codes de statut sont finis : je peux (et je l'ai fait) les rechercher et définir des types pour eux, et utiliser ces types pour restreindre le paramètre de type TStatus :
// Iteration 3. export type OkStatusCode = 200 | 201 | 202 | ...; export type ClientErrorStatusCode = 400 | 401 | 403 | ...; export type ServerErrorStatusCode = 500 | 501 | 502 | ...; export type StatusCode = OkStatusCode | ClientErrorStatusCode | ServerErrorStatusCode; export type NonOkStatusCode = Exclude<StatusCode, OkStatusCode>; export type FetchResponse<TStatus extends StatusCode, TBody> = { ok: TStatus extends OkStatusCode ? true : false; status: TStatus; statusText: string; body: TBody }; export type FetchResult<T, TStatus extends StatusCode, TBody> = T | FetchResponse<TStatus, TBody>;
Nous sommes arrivés à une série de types qui sont tout simplement magnifiques : en créant un branchement (en écrivant des instructions if) en fonction des conditions de la propriété ok ou status, la fonction de réduction de type de TypeScript entrera en jeu ! Si vous n'y croyez pas, écrivons la partie cours et essayons-la :
export class DrFetch<T> { for<TStatus extends StatusCode, TBody>() { return this as DrFetch<FetchResult<T, TStatus, TBody>>; } }
Testez ceci :
// For the 200 response, a copy of the updated object: { "id": 123, "text": "The updated text" } // For the 400 response, a list of validation errors: { "errors": [ "The updated text exceeds the maximum allowed number of characters." ] }
Il devrait maintenant être clair pourquoi le rétrécissement du type sera capable de prédire correctement la forme du corps lors du branchement, sur la base de la propriété ok de la propriété status.
Il y a cependant un problème : le typage initial de la classe lors de son instanciation, marqué dans le bloc de commentaire ci-dessus. Je l'ai résolu comme ceci :
export type AccumType<T, NewT> = T | NewT;
Ce petit changement exclut effectivement la saisie initiale, et nous sommes maintenant en activité !
Nous pouvons désormais écrire du code comme celui-ci, et Intellisense sera précis à 100 % :
export class NamePending<T> { accumulate<NewT>() { return this as NamePending<AccumType<T, NewT>>; } } // Now you can use it like this: const x = new NamePending<{ a: number; }>(); // x is of type NamePending<{ a: number; }>. const y = x.accumulate<{ b: string; }> // y is of type NamePending<{ a: number; } | { b: string; }>.
Le rétrécissement du type fonctionnera également lors de la requête de la propriété ok.
Si vous ne l'avez pas remarqué, nous avons pu écrire un code bien meilleur en ne générant pas d'erreurs. D'après mon expérience professionnelle, axios a tort, ky a tort et tout autre assistant de récupération faisant la même chose a tort.
Conclusion
TypeScript est vraiment amusant. En combinant TypeScript et une syntaxe fluide, nous sommes en mesure d'accumuler autant de types que nécessaire afin de pouvoir écrire un code plus précis et plus clair dès le premier jour, et non après un débogage répété. Cette technique a fait ses preuves et peut être essayée par tous. Installez dr-fetch et testez-le :
// Iteration 1. export type FetchResult<T, NewT> = T | NewT; // Iteration 2. export type FetchResponse<TStatus extends number, TBody> = { ok: boolean; status: TStatus; statusText: string; body: TBody }; export type FetchResult<T, TStatus extends number, NewT> = T | FetchResponse<TStatus, NewT>; //Makes sense to rename NewT to TBody.
Un package plus complexe
J'ai également créé wj-config, un package qui vise l'élimination complète des fichiers .env et dotenv obsolètes. Ce package utilise également l'astuce TypeScript enseignée ici, mais il joint les types avec &, pas |. Si vous souhaitez l'essayer, installez la v3.0.0-beta.1. Les saisies sont cependant beaucoup plus complexes. Faire dr-fetch après wj-config était une promenade dans le parc.
Trucs amusants : ce qu'il y a là-bas
Voyons quelques-unes des erreurs présentes dans les packages liés à la récupération.
récupération isomorphe
Vous pouvez voir dans le README ceci :
// Iteration 3. export type OkStatusCode = 200 | 201 | 202 | ...; export type ClientErrorStatusCode = 400 | 401 | 403 | ...; export type ServerErrorStatusCode = 500 | 501 | 502 | ...; export type StatusCode = OkStatusCode | ClientErrorStatusCode | ServerErrorStatusCode; export type NonOkStatusCode = Exclude<StatusCode, OkStatusCode>; export type FetchResponse<TStatus extends StatusCode, TBody> = { ok: TStatus extends OkStatusCode ? true : false; status: TStatus; statusText: string; body: TBody }; export type FetchResult<T, TStatus extends StatusCode, TBody> = T | FetchResponse<TStatus, TBody>;
« Mauvaise réponse du serveur » ?? Non. "Le serveur dit que votre demande est mauvaise". Oui, la partie lancer en elle-même est terrible.
ts-fetch
Celui-ci a la bonne idée, mais ne peut malheureusement taper que des réponses OK vs non-OK (2 types maximum).
ky
L'un des packages que j'ai le plus critiqué montre cet exemple :
export class DrFetch<T> { for<TStatus extends StatusCode, TBody>() { return this as DrFetch<FetchResult<T, TStatus, TBody>>; } }
Voici ce qu'écrirait un développeur très junior : Juste le chemin du bonheur. L'équivalence, selon son README :
const x = new DrFetch<{}>(); // Ok, having to write an empty type is inconvenient. const y = x .for<200, { a: string; }>() .for<400, { errors: string[]; }>() ; /* y's type: DrFetch<{ ok: true; status: 200; statusText: string; body: { a: string; }; } | { ok: false; status: 400; statusText: string; body: { errors: string[]; }; } | {} // <-------- WHAT IS THIS!!!??? > */
La partie lancer est tellement mauvaise : Pourquoi voudriez-vous vous brancher pour lancer, pour vous forcer à rattraper plus tard ? Cela n’a aucun sens pour moi. Le texte de l’erreur est également trompeur : il ne s’agit pas d’une « erreur de récupération ». La récupération a fonctionné. Vous avez eu une réponse, n'est-ce pas ? Vous n’avez tout simplement pas aimé ça… parce que ce n’est pas le chemin du bonheur. Une meilleure formulation serait « Échec de la requête HTTP : ». Ce qui a échoué, c'est la demande elle-même, pas l'opération de récupération.
Ce qui précède est le contenu détaillé de. pour plus d'informations, suivez d'autres articles connexes sur le site Web de PHP en chinois!

Outils d'IA chauds

Undresser.AI Undress
Application basée sur l'IA pour créer des photos de nu réalistes

AI Clothes Remover
Outil d'IA en ligne pour supprimer les vêtements des photos.

Undress AI Tool
Images de déshabillage gratuites

Clothoff.io
Dissolvant de vêtements AI

Video Face Swap
Échangez les visages dans n'importe quelle vidéo sans effort grâce à notre outil d'échange de visage AI entièrement gratuit !

Article chaud

Outils chauds

Bloc-notes++7.3.1
Éditeur de code facile à utiliser et gratuit

SublimeText3 version chinoise
Version chinoise, très simple à utiliser

Envoyer Studio 13.0.1
Puissant environnement de développement intégré PHP

Dreamweaver CS6
Outils de développement Web visuel

SublimeText3 version Mac
Logiciel d'édition de code au niveau de Dieu (SublimeText3)

Sujets chauds

Des questions et des solutions fréquemment posées pour l'impression de billets thermiques frontaux pour le développement frontal, l'impression de billets est une exigence commune. Cependant, de nombreux développeurs mettent en œuvre ...

JavaScript est la pierre angulaire du développement Web moderne, et ses principales fonctions incluent la programmation axée sur les événements, la génération de contenu dynamique et la programmation asynchrone. 1) La programmation axée sur les événements permet aux pages Web de changer dynamiquement en fonction des opérations utilisateur. 2) La génération de contenu dynamique permet d'ajuster le contenu de la page en fonction des conditions. 3) La programmation asynchrone garantit que l'interface utilisateur n'est pas bloquée. JavaScript est largement utilisé dans l'interaction Web, les applications à une page et le développement côté serveur, améliorant considérablement la flexibilité de l'expérience utilisateur et du développement multiplateforme.

Il n'y a pas de salaire absolu pour les développeurs Python et JavaScript, selon les compétences et les besoins de l'industrie. 1. Python peut être davantage payé en science des données et en apprentissage automatique. 2. JavaScript a une grande demande dans le développement frontal et complet, et son salaire est également considérable. 3. Les facteurs d'influence comprennent l'expérience, la localisation géographique, la taille de l'entreprise et les compétences spécifiques.

La discussion sur la réalisation des effets de défilement de parallaxe et d'animation des éléments dans cet article explorera comment réaliser le site officiel de Shiseido (https://www.shiseido.co.jp/sb/wonderland/) ...

Les dernières tendances de JavaScript incluent la montée en puissance de TypeScript, la popularité des frameworks et bibliothèques modernes et l'application de WebAssembly. Les prospects futurs couvrent des systèmes de type plus puissants, le développement du JavaScript côté serveur, l'expansion de l'intelligence artificielle et de l'apprentissage automatique, et le potentiel de l'informatique IoT et Edge.

Apprendre JavaScript n'est pas difficile, mais c'est difficile. 1) Comprendre les concepts de base tels que les variables, les types de données, les fonctions, etc. 2) Master la programmation asynchrone et les implémenter via des boucles d'événements. 3) Utilisez les opérations DOM et promettez de gérer les demandes asynchrones. 4) Évitez les erreurs courantes et utilisez des techniques de débogage. 5) Optimiser les performances et suivre les meilleures pratiques.

Comment fusionner les éléments du tableau avec le même ID dans un seul objet en JavaScript? Lors du traitement des données, nous rencontrons souvent la nécessité d'avoir le même ID ...

Problèmes de mise à jour des données dans les opérations asynchrones de Zustand. Lorsque vous utilisez la bibliothèque de gestion de l'État de Zustand, vous rencontrez souvent le problème des mises à jour de données qui entraînent des opérations asynchrones prématurées. � ...
