Heim >Web-Frontend >js-Tutorial >So verwenden Sie TypeScript zum Akkumulieren von Typen: Eingabe ALLER möglichen fetch()-Ergebnisse
Als ich (mit meinem Team) anfing, unsere Anwendung in TypeScript und Svelte neu zu schreiben (es war in JavaScript und React, was wir alle hassen), stand ich vor einem Problem:
Wie kann ich alle möglichen Textkörper einer HTTP-Antwort sicher eingeben?
Kommt Ihnen das bekannt? Wenn nicht, bist du höchstwahrscheinlich „einer von denen“, hehe. Lassen Sie uns einen Moment abschweifen, um das Bild besser zu verstehen.
Niemand scheint sich für „alle möglichen Körper“ einer HTTP-Antwort zu interessieren, da ich nichts finden konnte, was bereits dafür gemacht wurde (naja, vielleicht ts-fetch). Lassen Sie mich hier kurz meine Logik durchgehen, warum das so ist.
Niemand kümmert sich darum, weil die Leute auch:
Kümmere dich nur um den glücklichen Weg: Der Antworttext, wenn der HTTP-Statuscode 2xx ist.
Die Leute geben es manuell woanders ein.
Zu Punkt 1 würde ich sagen: Ja, Entwickler (insbesondere die unerfahrenen) vergessen, dass eine HTTP-Anfrage fehlschlagen kann und dass die in der fehlgeschlagenen Antwort enthaltenen Informationen höchstwahrscheinlich völlig anders sind als die reguläre Antwort.
Für #2 wollen wir uns mit einem großen Problem befassen, das in beliebten NPM-Paketen wie ky und axios auftritt.
Soweit ich das beurteilen kann, mögen Leute Pakete wie ky oder axios, weil eines ihrer „Features“ darin besteht, dass sie bei nicht-OK-HTTP-Statuscodes einen Fehler auslösen. Seit wann ist das in Ordnung? Da noch nie. Aber offenbar greifen die Leute das nicht auf. Die Leute sind glücklich und zufrieden, wenn sie Fehler bei nicht OK-Antworten erhalten.
Ich stelle mir vor, dass Leute nicht-OK-Körper eingeben, wenn es Zeit zum Fangen ist. Was für ein Durcheinander, was für ein Code-Geruch!
Das ist ein Codegeruch, weil Sie try..catch-Blöcke effektiv als Verzweigungsanweisungen verwenden und try..catch nicht als Verzweigungsanweisung gedacht ist.
Aber selbst wenn Sie mit mir argumentieren würden, dass die Verzweigung in try..catch auf natürliche Weise erfolgt, gibt es einen weiteren wichtigen Grund, warum dies weiterhin schlecht ist: Wenn ein Fehler ausgegeben wird, muss die Laufzeit den Aufrufstapel abwickeln. Dies ist im Hinblick auf die CPU-Zyklen weitaus kostspieliger als eine normale Verzweigung mit einer if- oder switch-Anweisung.
Können Sie in diesem Wissen den Leistungseinbruch rechtfertigen, nur um den try..catch-Block zu missbrauchen? Ich sage nein. Ich kann mir keinen einzigen Grund vorstellen, warum die Front-End-Welt damit vollkommen zufrieden zu sein scheint.
Nachdem ich nun meine Argumentation erläutert habe, kehren wir zum Hauptthema zurück.
Eine HTTP-Antwort kann je nach Statuscode unterschiedliche Informationen enthalten. Beispielsweise kann ein Todo-Endpunkt wie api/todos/:id, der eine PATCH-HTTP-Anfrage empfängt, eine Antwort mit einem anderen Text zurückgeben, wenn der Statuscode der Antwort 200 ist, als wenn der Statuscode der Antwort 400 ist.
Lass es uns veranschaulichen:
// 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." ] }
Vor diesem Hintergrund kehren wir zur Problemstellung zurück: Wie kann ich eine Funktion eingeben, die diese PATCH-Anfrage ausführt, wobei TypeScript mir je nach HTTP-Statuscode beim Schreiben mitteilen kann, mit welchem Body ich es zu tun habe? Code? Die Antwort: Verwenden Sie eine fließende Syntax (Builder-Syntax, verkettete Syntax), um Typen zu akkumulieren.
Beginnen wir mit der Definition eines Typs, der auf einem vorherigen Typ aufbaut:
export type AccumType<T, NewT> = T | NewT;
Super einfach: Verbinde die gegebenen Typen T und NewT zu einem neuen Typ. Verwenden Sie diesen neuen Typ erneut als T in AccumType<>, und Sie können dann einen weiteren neuen Typ akkumulieren. Das per Hand zu machen ist allerdings nicht schön. Lassen Sie uns ein weiteres Schlüsselelement für die Lösung vorstellen: Fließende Syntax.
Angenommen ein Objekt der Klasse X, dessen Methoden sich immer selbst (oder eine Kopie von sich selbst) zurückgeben, kann man Methodenaufrufe nacheinander verketten. Dies ist eine fließende Syntax oder verkettete Syntax.
Lassen Sie uns eine einfache Klasse schreiben, die Folgendes tut:
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; }>.
Eureka! Wir haben die fließende Syntax und den von uns geschriebenen Typ erfolgreich kombiniert, um mit der Akkumulation von Datentypen in einem einzigen Typ zu beginnen!
Falls es nicht offensichtlich ist, können Sie die Übung fortsetzen, bis Sie die gewünschten Typen angesammelt haben (x.accumulate().accumulate()… bis Sie fertig sind).
Das ist alles gut und schön, aber dieser supereinfache Typ bindet den HTTP-Statuscode nicht an den entsprechenden Body-Typ.
Was wir wollen, ist, TypeScript mit genügend Informationen zu versorgen, damit seine Funktion zur Typeingrenzung greift. Dazu tun wir das Notwendige, um Code zu erhalten, der für das ursprüngliche Problem relevant ist (Eingabe von Texten von HTTP-Antworten in eine per -Statuscodebasis).
Zunächst benennen Sie AccumType um und entwickeln Sie es weiter. Der folgende Code zeigt den Fortschritt in Iterationen:
// 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.
An diesem Punkt wurde mir etwas klar: Statuscodes sind endlich: Ich kann (und habe) sie nachschlagen und Typen für sie definieren und diese Typen verwenden, um den Typparameter TStatus einzuschränken:
// 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>;
Wir sind bei einer Reihe von Typen angelangt, die einfach wunderschön sind: Durch Verzweigen (Schreiben von if-Anweisungen) basierend auf Bedingungen für die ok- oder die status-Eigenschaft wird die Typeingrenzungsfunktion von TypeScript aktiviert! Wenn Sie es nicht glauben, schreiben wir den Unterrichtsteil und probieren es aus:
export class DrFetch<T> { for<TStatus extends StatusCode, TBody>() { return this as DrFetch<FetchResult<T, TStatus, TBody>>; } }
Probefahren Sie dies:
// 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." ] }
Jetzt sollte klar sein, warum die Typeingrenzung die Form des Körpers beim Verzweigen basierend auf der ok-Eigenschaft der Statuseigenschaft korrekt vorhersagen kann.
Es gibt jedoch ein Problem: Die anfängliche Typisierung der Klasse, wenn sie instanziiert wird, markiert im Kommentarblock oben. Ich habe es so gelöst:
export type AccumType<T, NewT> = T | NewT;
Diese kleine Änderung macht das anfängliche Tippen praktisch überflüssig, und wir sind jetzt im Geschäft!
Jetzt können wir Code wie den folgenden schreiben und Intellisense wird 100 % genau sein:
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; }>.
Typeingrenzung funktioniert auch bei der Abfrage der ok-Eigenschaft.
Wenn Sie es nicht bemerkt haben: Wir konnten viel besseren Code schreiben, indem wir keine Fehler auslösten. Nach meiner beruflichen Erfahrung ist Axios falsch, Ky ist falsch und jeder andere Fetch-Helfer da draußen, der das Gleiche tut, ist falsch.
TypeScript macht tatsächlich Spaß. Durch die Kombination von TypeScript und fließender Syntax können wir so viele Typen wie nötig ansammeln, sodass wir vom ersten Tag an genaueren und klareren Code schreiben können, nicht erst nach mehrmaligem Debuggen. Diese Technik hat sich als erfolgreich erwiesen und kann von jedem ausprobiert werden. Installieren Sie dr-fetch und testen Sie es:
// 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.
Ich habe auch wj-config erstellt, ein Paket, das auf die vollständige Beseitigung der veralteten .env-Dateien und dotenv abzielt. Dieses Paket verwendet ebenfalls den hier gelehrten TypeScript-Trick, verknüpft jedoch Typen mit & und nicht mit |. Wenn Sie es ausprobieren möchten, installieren Sie v3.0.0-beta.1. Allerdings sind die Eingaben deutlich komplexer. dr-fetch nach wj-config zu erstellen war ein Kinderspiel.
Sehen wir uns einige der Fehler an, die es in Abruf-bezogenen Paketen gibt.
Sie können dies in der README-Datei sehen:
// 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>;
„Schlechte Antwort vom Server“?? Nein. „Der Server sagt, Ihre Anfrage sei fehlerhaft.“ Ja, der Wurfteil selbst ist schrecklich.
Dieser hat die richtige Idee, kann aber leider nur OK- oder Nicht-OK-Antworten eingeben (maximal 2 Typen).
Eines der Pakete, das ich am meisten kritisiert habe, zeigt dieses Beispiel:
export class DrFetch<T> { for<TStatus extends StatusCode, TBody>() { return this as DrFetch<FetchResult<T, TStatus, TBody>>; } }
Das würde ein sehr junger Entwickler schreiben: Nur der glückliche Weg. Die Äquivalenz laut 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!!!??? > */
Der Teil des Werfens ist so schlimm: Warum solltest du dich zum Werfen verzweigen, um dich später zum Fangen zu zwingen? Für mich macht es überhaupt keinen Sinn. Auch der Text im Fehler ist irreführend: Es handelt sich nicht um einen „Abruffehler“. Der Abruf hat funktioniert. Du hast eine Antwort bekommen, nicht wahr? Es hat dir einfach nicht gefallen ... weil es nicht der glückliche Weg ist. Eine bessere Formulierung wäre „HTTP-Anfrage fehlgeschlagen:“. Was fehlgeschlagen ist, war die Anfrage selbst, nicht der Abrufvorgang.
Das obige ist der detaillierte Inhalt vonSo verwenden Sie TypeScript zum Akkumulieren von Typen: Eingabe ALLER möglichen fetch()-Ergebnisse. Für weitere Informationen folgen Sie bitte anderen verwandten Artikeln auf der PHP chinesischen Website!