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.
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.
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.
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.
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.
É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.
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.
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.
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.
Voyons quelques-unes des erreurs présentes dans les packages liés à la récupération.
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.
Celui-ci a la bonne idée, mais ne peut malheureusement taper que des réponses OK vs non-OK (2 types maximum).
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!