Dynamisches statisches Tippen in TypeScript

Veröffentlicht: 2022-03-10
Kurze Zusammenfassung ↬ In diesem Artikel betrachten wir einige der fortgeschritteneren Funktionen von TypeScript, wie Union-Typen, bedingte Typen, Template-Literaltypen und Generika. Wir möchten das dynamischste JavaScript-Verhalten so formalisieren, dass wir die meisten Fehler abfangen können, bevor sie auftreten. Wir wenden mehrere Erkenntnisse aus allen Kapiteln von TypeScript in 50 Lessons an, einem Buch, das wir Ende 2020 hier im Smashing Magazine veröffentlicht haben. Wenn Sie daran interessiert sind, mehr zu erfahren, sollten Sie es sich unbedingt ansehen!

JavaScript ist eine von Natur aus dynamische Programmiersprache. Wir als Entwickler können mit wenig Aufwand viel ausdrücken, und die Sprache und ihre Laufzeit finden heraus, was wir uns vorgenommen haben. Das macht JavaScript bei Anfängern so beliebt und macht erfahrene Entwickler produktiv! Es gibt jedoch eine Einschränkung: Wir müssen wachsam sein! Fehler, Tippfehler, korrektes Programmverhalten: Vieles davon spielt sich in unseren Köpfen ab!

Sehen Sie sich das folgende Beispiel an.

 app.get("/api/users/:userID", function(req, res) { if (req.method === "POST") { res.status(20).send({ message: "Got you, user " + req.params.userId }); } })

Wir haben einen Server im Stil von https://expressjs.com/, der es uns ermöglicht, eine Route (oder einen Pfad) zu definieren und einen Rückruf ausführt, wenn die URL angefordert wird.

Der Rückruf akzeptiert zwei Argumente:

  1. Das request .
    Hier erhalten wir Informationen über die verwendete HTTP-Methode (z. B. GET, POST, PUT, DELETE) und zusätzliche Parameter, die eingehen. In diesem Beispiel sollte userID einem Parameter userID werden, der, nun ja, die ID des Benutzers enthält!
  2. Das response oder reply .
    Hier wollen wir eine richtige Antwort vom Server an den Client vorbereiten. Wir möchten korrekte Statuscodes (method status ) senden und die JSON-Ausgabe über die Leitung senden.

Was wir in diesem Beispiel sehen, ist stark vereinfacht, gibt aber eine gute Vorstellung davon, was wir vorhaben. Auch das obige Beispiel ist voller Fehler! Guck mal:

 app.get("/api/users/:userID", function(req, res) { if (req.method === "POST") { /* Error 1 */ res.status(20).send({ /* Error 2 */ message: "Welcome, user " + req.params.userId /* Error 3 */ }); } })

Oh wow! Drei Zeilen Implementierungscode und drei Fehler? Was ist passiert?

  1. Der erste Fehler ist nuanciert. Während wir unserer App mitteilen, dass wir GET -Anforderungen abhören möchten (daher app.get ), tun wir nur etwas, wenn die Anforderungsmethode POST ist. An diesem bestimmten Punkt in unserer Anwendung kann req.method nicht POST sein. Wir würden also niemals eine Antwort senden, was zu unerwarteten Zeitüberschreitungen führen könnte.
  2. Toll, dass wir explizit einen Statuscode versenden! 20 ist jedoch kein gültiger Statuscode. Kunden verstehen möglicherweise nicht, was hier passiert.
  3. Dies ist die Antwort, die wir zurücksenden möchten. Wir greifen auf die geparsten Argumente zu, haben aber einen fiesen Tippfehler. Es ist userID nicht userId . Alle unsere Benutzer würden mit „Willkommen, Benutzer undefiniert!“ begrüßt. Etwas, das Sie definitiv in freier Wildbahn gesehen haben!

Und so etwas passiert! Vor allem in JavaScript. Wir gewinnen an Ausdruckskraft – wir mussten uns nicht ein einziges Mal um Typen kümmern – sondern genau aufpassen, was wir tun.

Dies ist auch der Punkt, an dem JavaScript viel Gegenreaktion von Programmierern erfährt, die nicht an dynamische Programmiersprachen gewöhnt sind. Sie haben normalerweise Compiler, die sie auf mögliche Probleme hinweisen und Fehler im Voraus abfangen. Sie könnten hochnäsig wirken, wenn sie die Menge an zusätzlicher Arbeit missbilligen, die Sie in Ihrem Kopf erledigen müssen, um sicherzustellen, dass alles richtig funktioniert. Sie könnten Ihnen sogar sagen, dass JavaScript keine Typen hat. Was nicht wahr ist.

Anders Hejlsberg, der leitende Architekt von TypeScript, sagte in seiner Keynote zu MS Build 2017: „ Es ist nicht so, dass JavaScript kein Typsystem hat. Es gibt einfach keine Möglichkeit, es zu formalisieren “.

Und das ist der Hauptzweck von TypeScript. TypeScript möchte Ihren JavaScript-Code besser verstehen als Sie. Und wo TypeScript nicht herausfinden kann, was Sie meinen, können Sie helfen, indem Sie zusätzliche Typinformationen bereitstellen.

Mehr nach dem Sprung! Lesen Sie unten weiter ↓

Grundlegende Eingabe

Und das werden wir jetzt tun. Nehmen wir die get -Methode von unserem Express-Server und fügen genügend Typinformationen hinzu, damit wir so viele Fehlerkategorien wie möglich ausschließen können.

Wir beginnen mit einigen grundlegenden Typinformationen. Wir haben ein app -Objekt, das auf eine get -Funktion zeigt. Die get -Funktion nimmt path , bei dem es sich um eine Zeichenfolge handelt, und einen Rückruf.

 const app = { get, /* post, put, delete, ... to come! */ }; function get(path: string, callback: CallbackFn) { // to be implemented --> not important right now }

Während string ein grundlegender, sogenannter primitiver Typ ist, ist CallbackFn ein zusammengesetzter Typ, den wir explizit definieren müssen.

CallbackFn ist ein Funktionstyp, der zwei Argumente akzeptiert:

  • req , die vom Typ ServerRequest ist
  • reply , die vom Typ ServerReply ist

CallbackFn gibt void zurück.

 type CallbackFn = (req: ServerRequest, reply: ServerReply) => void;

ServerRequest ist in den meisten Frameworks ein ziemlich komplexes Objekt. Wir machen eine vereinfachte Version zu Demonstrationszwecken. Wir übergeben eine method für "GET" , "POST" , "PUT" , "DELETE" usw. Sie hat auch einen params -Eintrag. Datensätze sind Objekte, die eine Reihe von Schlüsseln einer Reihe von Eigenschaften zuordnen. Im Moment wollen wir zulassen, dass jeder string Schlüssel einer string Eigenschaft zugeordnet wird. Wir überarbeiten dies später.

 type ServerRequest = { method: string; params: Record<string, string>; };

Für ServerReply haben wir einige Funktionen ausgelegt, da wir wissen, dass ein echtes ServerReply Objekt viel mehr hat. Eine Sendefunktion nimmt ein optionales Argument mit den Daten, die wir send möchten. Und wir haben die Möglichkeit, mit der status einen Statuscode zu setzen.

 type ServerReply = { send: (obj?: any) => void; status: (statusCode: number) => ServerReply; };

Das ist schon etwas, und wir können ein paar Fehler ausschließen:

 app.get("/api/users/:userID", function(req, res) { if(req.method === 2) { // ^^^^^^^^^^^^^^^^^ Error, type number is not assignable to string res.status("200").send() // ^^^^^ Error, type string is not assignable to number } })

Aber wir können immer noch falsche Statuscodes senden (jede Zahl ist möglich) und haben keine Ahnung von den möglichen HTTP-Methoden (jeder String ist möglich). Lassen Sie uns unsere Typen verfeinern.

Kleinere Sätze

Sie können primitive Typen als eine Menge aller möglichen Werte dieser bestimmten Kategorie sehen. Beispielsweise enthält string alle möglichen Zeichenfolgen, die in JavaScript ausgedrückt werden können, number enthält alle möglichen Zahlen mit doppelter Float-Präzision. boolean schließt alle möglichen booleschen Werte ein, die true und false sind.

Mit TypeScript können Sie diese Mengen auf kleinere Teilmengen verfeinern. Beispielsweise können wir einen Typ Method erstellen, der alle möglichen Zeichenfolgen enthält, die wir für HTTP-Methoden erhalten können:

 type Methods= "GET" | "POST" | "PUT" | "DELETE"; type ServerRequest = { method: Methods; params: Record<string, string>; };

Method ist ein kleinerer Satz des größeren string . Method ist ein Vereinigungstyp von Literaltypen. Ein Literaltyp ist die kleinste Einheit einer gegebenen Menge. Eine wörtliche Zeichenfolge. Eine wörtliche Zahl. Es gibt keine Zweideutigkeit. Es ist nur "GET" . Sie fügen sie in eine Vereinigung mit anderen Literaltypen ein und erstellen so eine Teilmenge aller größeren Typen, die Sie haben. Sie können auch eine Teilmenge mit Literaltypen von string und number oder verschiedenen zusammengesetzten Objekttypen erstellen. Es gibt viele Möglichkeiten, Literaltypen zu kombinieren und in Unions einzufügen.

Dies wirkt sich sofort auf unseren Server-Callback aus. Plötzlich können wir zwischen diesen vier Methoden (oder mehr, wenn nötig) unterscheiden und alle Möglichkeiten im Code ausschöpfen. TypeScript führt uns:

 app.get("/api/users/:userID", function (req, res) { // at this point, TypeScript knows that req.method // can take one of four possible values switch (req.method) { case "GET": break; case "POST": break; case "DELETE": break; case "PUT": break; default: // here, req.method is never req.method; } });

Bei jeder case -Anweisung, die Sie machen, kann TypeScript Ihnen Informationen zu den verfügbaren Optionen geben. Probieren Sie es selbst aus. Wenn Sie alle Optionen ausgeschöpft haben, teilt Ihnen TypeScript in Ihrem default mit, dass dies never passieren kann. Dies ist buchstäblich der Typ never , was bedeutet, dass Sie möglicherweise einen Fehlerzustand erreicht haben, den Sie behandeln müssen.

Das ist eine Fehlerkategorie weniger. Wir wissen jetzt genau, welche möglichen HTTP-Methoden zur Verfügung stehen.

Wir können dasselbe für HTTP-Statuscodes tun, indem wir eine Teilmenge gültiger Zahlen definieren, die statusCode kann:

 type StatusCode = 100 | 101 | 102 | 200 | 201 | 202 | 203 | 204 | 205 | 206 | 207 | 208 | 226 | 300 | 301 | 302 | 303 | 304 | 305 | 306 | 307 | 308 | 400 | 401 | 402 | 403 | 404 | 405 | 406 | 407 | 408 | 409 | 410 | 411 | 412 | 413 | 414 | 415 | 416 | 417 | 418 | 420 | 422 | 423 | 424 | 425 | 426 | 428 | 429 | 431 | 444 | 449 | 450 | 451 | 499 | 500 | 501 | 502 | 503 | 504 | 505 | 506 | 507 | 508 | 509 | 510 | 511 | 598 | 599; type ServerReply = { send: (obj?: any) => void; status: (statusCode: StatusCode) => ServerReply; };

Typ StatusCode ist wieder ein Union-Typ. Und damit schließen wir eine weitere Kategorie von Fehlern aus. Plötzlich schlägt Code wie dieser fehl:

 app.get("/api/user/:userID", (req, res) => { if(req.method === "POS") { // ^^^^^^^^^^^^^^^^^^^ 'Methods' and '"POS"' have no overlap. res.status(20) // ^^ '20' is not assignable to parameter of type 'StatusCode' } })
Und unsere Software wird viel sicherer! Aber wir können noch mehr!

Generika eingeben

Wenn wir eine Route mit app.get definieren, wissen wir implizit, dass die einzig mögliche HTTP-Methode "GET" ist. Aber bei unseren Typdefinitionen müssen wir noch nach allen möglichen Teilen der Vereinigung suchen.

Der Typ für CallbackFn ist korrekt, da wir Callback-Funktionen für alle möglichen HTTP-Methoden definieren könnten, aber wenn wir app.get explizit aufrufen, wäre es schön, einige zusätzliche Schritte zu sparen, die nur notwendig sind, um Typisierungen zu erfüllen.

TypeScript-Generika können helfen! Generics sind eines der wichtigsten Features in TypeScript, mit denen Sie das dynamischste Verhalten aus statischen Typen herausholen können. In TypeScript in 50 Lektionen verbringen wir die letzten drei Kapitel damit, uns mit allen Feinheiten von Generika und ihrer einzigartigen Funktionalität zu befassen.

Was Sie jetzt wissen müssen, ist, dass wir ServerRequest so definieren möchten, dass wir einen Teil der Methods anstelle des gesamten Satzes angeben können. Dazu verwenden wir die generische Syntax, in der wir Parameter wie bei Funktionen definieren können:

 type ServerRequest<Met extends Methods> = { method: Met; params: Record<string, string>; };

Das ist, was passiert:

  1. ServerRequest wird zu einem generischen Typ, wie durch die spitzen Klammern angegeben
  2. Wir definieren einen generischen Parameter namens Met , der eine Teilmenge des Typs Methods ist
  3. Wir verwenden diesen generischen Parameter als generische Variable, um die Methode zu definieren.

Ich ermutige Sie auch, meinen Artikel über die Benennung generischer Parameter zu lesen.

Mit dieser Änderung können wir verschiedene ServerRequest s angeben, ohne Dinge zu duplizieren:

 type OnlyGET = ServerRequest<"GET">; type OnlyPOST = ServerRequest<"POST">; type POSTorPUT = ServerRquest<"POST" | "PUT">;

Da wir die Schnittstelle von ServerRequest geändert haben, müssen wir Änderungen an all unseren anderen Typen vornehmen, die ServerRequest verwenden, wie CallbackFn und die Funktion get :

 type CallbackFn<Met extends Methods> = ( req: ServerRequest<Met>, reply: ServerReply ) => void; function get(path: string, callback: CallbackFn<"GET">) { // to be implemented }

Mit der get -Funktion übergeben wir ein tatsächliches Argument an unseren generischen Typ. Wir wissen, dass dies nicht nur eine Teilmenge von Methods sein wird, wir wissen genau, mit welcher Teilmenge wir es zu tun haben.

Wenn wir jetzt app.get verwenden, haben wir nur einen möglichen Wert für req.method :

 app.get("/api/users/:userID", function (req, res) { req.method; // can only be get });

Dadurch wird sichergestellt, dass wir nicht davon ausgehen, dass HTTP-Methoden wie "POST" oder ähnliches verfügbar sind, wenn wir einen app.get -Callback erstellen. Wir wissen genau, womit wir es an dieser Stelle zu tun haben, also spiegeln wir das in unseren Typen wider.

Wir haben bereits viel dafür getan, dass request.method vernünftig typisiert ist und den tatsächlichen Stand der Dinge darstellt. Ein netter Vorteil, den wir durch das Unterteilen des Union-Typs Methods erhalten, besteht darin, dass wir eine Allzweck-Callback-Funktion außerhalb von app.get erstellen können, die typsicher ist:

 const handler: CallbackFn<"PUT" | "POST"> = function(res, req) { res.method // can be "POST" or "PUT" }; const handlerForAllMethods: CallbackFn<Methods> = function(res, req) { res.method // can be all methods }; app.get("/api", handler); // ^^^^^^^ Nope, we don't handle "GET" app.get("/api", handlerForAllMethods); // This works

Parameter eingeben

Was wir noch nicht berührt haben, ist die Eingabe des params Objekts. Bisher erhalten wir einen Datensatz, der den Zugriff auf jeden string ermöglicht. Unsere Aufgabe ist es nun, das etwas konkreter zu machen!

Dazu fügen wir eine weitere generische Variable hinzu. Eine für Methoden, eine für die möglichen Schlüssel in unserem Record :

 type ServerRequest<Met extends Methods, Par extends string = string> = { method: Met; params: Record<Par, string>; };

Die generische Typvariable Par kann eine Teilmenge des Typs string sein, und der Standardwert ist jede Zeichenfolge. Damit können wir ServerRequest mitteilen, welche Schlüssel wir erwarten:

 // request.method = "GET" // request.params = { // userID: string // } type WithUserID = ServerRequest<"GET", "userID">

Lassen Sie uns das neue Argument zu unserer get -Funktion und dem CallbackFn -Typ hinzufügen, damit wir die angeforderten Parameter festlegen können:

 function get<Par extends string = string>( path: string, callback: CallbackFn<"GET", Par> ) { // to be implemented } type CallbackFn<Met extends Methods, Par extends string> = ( req: ServerRequest<Met, Par>, reply: ServerReply ) => void;

Wenn wir Par nicht explizit setzen, funktioniert der Typ wie gewohnt, da Par standardmäßig auf string gesetzt ist. Wenn wir es aber setzen, haben wir plötzlich eine richtige Definition für das req.params Objekt!

 app.get<"userID">("/api/users/:userID", function (req, res) { req.params.userID; // Works!! req.params.anythingElse; // doesn't work!! });

Das ist großartig! Es gibt jedoch eine Kleinigkeit, die verbessert werden kann. Wir können immer noch jeden String an das path von app.get . Wäre es nicht besser, wenn wir Par auch darin widerspiegeln könnten?

Wir können! Mit der Veröffentlichung von Version 4.1 ist TypeScript in der Lage, Template -Literaltypen zu erstellen. Syntaktisch funktionieren sie genauso wie String-Template-Literale, jedoch auf Typebene. Wo wir in der Lage waren, den Set string in Teilmengen mit String-Literaltypen aufzuteilen (wie wir es bei Methods getan haben), erlauben uns Template-Literaltypen, ein ganzes Spektrum von Strings einzuschließen.

Lassen Sie uns einen Typ namens IncludesRouteParams erstellen, in dem wir sicherstellen möchten, dass Par ordnungsgemäß in die Express-artige Methode zum Hinzufügen eines Doppelpunkts vor dem Parameternamen eingefügt wird:

 type IncludesRouteParams<Par extends string> = | `${string}/:${Par}` | `${string}/:${Par}/${string}`;

Der generische Typ IncludesRouteParams akzeptiert ein Argument, das eine Teilmenge von string ist. Es erstellt einen Vereinigungstyp aus zwei Vorlagenliteralen:

  1. Das erste Vorlagenliteral beginnt mit einer beliebigen string und enthält dann ein / -Zeichen, gefolgt von einem : -Zeichen, gefolgt vom Parameternamen. Dadurch wird sichergestellt, dass wir alle Fälle abfangen, in denen sich der Parameter am Ende der Routenzeichenfolge befindet.
  2. Das zweite Vorlagenliteral beginnt mit einer beliebigen string , gefolgt von demselben Muster aus / , : und dem Parameternamen. Dann haben wir ein weiteres / -Zeichen, gefolgt von einer beliebigen Zeichenfolge. Dieser Zweig des Union-Typs stellt sicher, dass wir alle Fälle abfangen, in denen sich der Parameter irgendwo innerhalb einer Route befindet.

So verhält sich IncludesRouteParams mit dem Parameternamen userID bei verschiedenen Testfällen:

 const a: IncludeRouteParams<"userID"> = "/api/user/:userID" // const a: IncludeRouteParams<"userID"> = "/api/user/:userID/orders" // const a: IncludeRouteParams<"userID"> = "/api/user/:userId" // const a: IncludeRouteParams<"userID"> = "/api/user" // const a: IncludeRouteParams<"userID"> = "/api/user/:userIDAndmore" //

Lassen Sie uns unseren neuen Utility-Typ in die get -Funktionsdeklaration aufnehmen.

 function get<Par extends string = string>( path: IncludesRouteParams<Par>, callback: CallbackFn<"GET", Par> ) { // to be implemented } app.get<"userID">( "/api/users/:userID", function (req, res) { req.params.userID; // YEAH! } );

Toll! Wir bekommen einen weiteren Sicherheitsmechanismus, um sicherzustellen, dass wir das Hinzufügen der Parameter zur tatsächlichen Route nicht verpassen! Wie mächtig.

Generische Bindungen

Aber stell dir vor, ich bin immer noch nicht glücklich damit. Es gibt ein paar Probleme mit diesem Ansatz, die in dem Moment deutlich werden, in dem Ihre Routen etwas komplexer werden.

  1. Das erste Problem, das ich habe, ist, dass wir unsere Parameter explizit im generischen Typparameter angeben müssen. Wir müssen Par an "userID" binden, obwohl wir es ohnehin im Pfadargument der Funktion angeben würden. Dies ist kein JavaScript-y!
  2. Dieser Ansatz behandelt nur einen Routenparameter. In dem Moment, in dem wir eine Union hinzufügen, zB "userID" | "orderId" "userID" | "orderId" ist die Failsafe-Prüfung zufrieden, wenn nur eines dieser Argumente verfügbar ist. So funktionieren Sets. Es kann das eine oder das andere sein.

Es muss einen besseren Weg geben. Und da ist. Andernfalls würde dieser Artikel mit einer sehr bitteren Note enden.

Lassen Sie uns die Reihenfolge umkehren! Versuchen wir nicht, die Routenparameter in einer generischen Typvariablen zu definieren, sondern extrahieren Sie die Variablen aus dem path , den wir als erstes Argument von app.get .

Um zum eigentlichen Wert zu gelangen, müssen wir uns ansehen, wie die generische Bindung in TypeScript funktioniert. Nehmen wir zum Beispiel diese identity :

 function identity<T>(inp: T) : T { return inp }

Es ist vielleicht die langweiligste generische Funktion, die Sie jemals gesehen haben, aber sie veranschaulicht einen Punkt perfekt. identity nimmt ein Argument und gibt dieselbe Eingabe erneut zurück. Der Typ ist der generische Typ T und gibt auch denselben Typ zurück.

Jetzt können wir T zum Beispiel an string binden:

 const z = identity<string>("yes"); // z is of type string

Diese explizit generische Bindung stellt sicher, dass wir nur strings an identity übergeben, und da wir explizit binden, ist der Rückgabetyp auch string . Wenn wir das Binden vergessen, passiert etwas Interessantes:

 const y = identity("yes") // y is of type "yes"

In diesem Fall leitet TypeScript den Typ aus dem übergebenen Argument ab und bindet T an den Zeichenfolgenliteraltyp "yes" . Dies ist eine großartige Möglichkeit, ein Funktionsargument in einen Literaltyp umzuwandeln, den wir dann in unseren anderen generischen Typen verwenden.

Tun wir das, indem app.get anpassen.

 function get<Path extends string = string>( path: Path, callback: CallbackFn<"GET", ParseRouteParams<Path>> ) { // to be implemented }

Wir entfernen den generischen Typ Par und fügen Path hinzu. Path kann eine Teilmenge einer beliebigen string sein. Wir setzen path auf diesen generischen Typ Path , was bedeutet, dass wir in dem Moment, in dem wir einen Parameter an get übergeben, seinen String-Literaltyp abfangen. Wir übergeben Path an einen neuen generischen Typ ParseRouteParams , den wir noch nicht erstellt haben.

Lassen Sie uns an ParseRouteParams . Hier tauschen wir die Reihenfolge der Ereignisse wieder um. Anstatt die angeforderten Routenparameter an das Generikum zu übergeben, um sicherzustellen, dass der Pfad in Ordnung ist, übergeben wir den Routenpfad und extrahieren die möglichen Routenparameter. Dazu müssen wir einen bedingten Typ erstellen.

Bedingte Typen und rekursive Template-Literaltypen

Bedingte Typen ähneln syntaktisch dem ternären Operator in JavaScript. Sie suchen nach einer Bedingung, und wenn die Bedingung erfüllt ist, geben Sie Zweig A zurück, andernfalls geben Sie Zweig B zurück. Zum Beispiel:

 type ParseRouteParams<Rte> = Rte extends `${string}/:${infer P}` ? P : never;

Hier prüfen wir, ob Rte eine Teilmenge jedes Pfads ist, der mit dem Parameter am Ende im Express-Stil endet (mit einem vorangestellten "/:" ). Wenn ja, leiten wir diese Zeichenfolge ab. Das heißt, wir erfassen seinen Inhalt in einer neuen Variablen. Wenn die Bedingung erfüllt ist, geben wir die neu extrahierte Zeichenfolge zurück, andernfalls geben wir nie zurück, wie in: „Es gibt keine Routenparameter“,

Wenn wir es ausprobieren, bekommen wir so etwas:

 type Params = ParseRouteParams<"/api/user/:userID"> // Params is "userID" type NoParams = ParseRouteParams<"/api/user"> // NoParams is never --> no params!

Toll, das ist schon viel besser als früher. Jetzt wollen wir alle anderen möglichen Parameter erfassen. Dafür müssen wir eine weitere Bedingung hinzufügen:

 type ParseRouteParams<Rte> = Rte extends `${string}/:${infer P}/${infer Rest}` ? P | ParseRouteParams<`/${Rest}`> : Rte extends `${string}/:${infer P}` ? P : never;

Unser Bedingungstyp funktioniert jetzt wie folgt:

  1. In der ersten Bedingung prüfen wir, ob es irgendwo zwischen der Route einen Routenparameter gibt. Wenn ja, extrahieren wir sowohl den Routenparameter als auch alles andere, was danach kommt. Wir geben den neu gefundenen Routenparameter P in einer Vereinigung zurück, in der wir denselben generischen Typ rekursiv mit Rest aufrufen. Wenn wir beispielsweise die Route "/api/users/:userID/orders/:orderID" an ParseRouteParams übergeben, leiten wir "userID" in P und "orders/:orderID" in Rest ab. Wir nennen den gleichen Typ mit Rest
  2. Hier kommt die zweite Bedingung ins Spiel. Hier prüfen wir, ob am Ende ein Typ steht. Dies ist bei "orders/:orderID" der Fall. Wir extrahieren "orderID" und geben diesen Literaltyp zurück.
  3. Wenn kein Routenparameter mehr übrig ist, geben wir nie zurück.

Dan Vanderkam zeigt einen ähnlichen und ausgefeilteren Typ für ParseRouteParams , aber der oben gezeigte sollte auch funktionieren. Wenn wir unsere neu angepassten ParseRouteParams , erhalten wir in etwa Folgendes:

 // Params is "userID" type Params = ParseRouteParams<"/api/user/:userID"> // MoreParams is "userID" | "orderID" type MoreParams = ParseRouteParams<"/api/user/:userID/orders/:orderId">

Lassen Sie uns diesen neuen Typ anwenden und sehen, wie unsere endgültige Verwendung von app.get aussieht.

 app.get("/api/users/:userID/orders/:orderID", function (req, res) { req.params.userID; // YES!! req.params.orderID; // Also YES!!! });

Beeindruckend. Das sieht genauso aus wie der JavaScript-Code, den wir am Anfang hatten!

Statische Typen für dynamisches Verhalten

Die Typen, die wir gerade für eine Funktion app.get erstellt haben, stellen sicher, dass wir eine Menge möglicher Fehler ausschließen:

  1. Wir können nur richtige numerische Statuscodes an res.status()
  2. req.method ist eine von vier möglichen Zeichenfolgen, und wenn wir app.get verwenden, wissen wir, dass es nur "GET" ist.
  3. Wir können Routenparameter parsen und sicherstellen, dass wir keine Tippfehler in unserem Callback haben

Wenn wir uns das Beispiel vom Anfang dieses Artikels ansehen, erhalten wir folgende Fehlermeldungen:

 app.get("/api/users/:userID", function(req, res) { if (req.method === "POST") { // ^^^^^^^^^^^^^^^^^^^^^ // This condition will always return 'false' // since the types '"GET"' and '"POST"' have no overlap. res.status(20).send({ // ^^ // Argument of type '20' is not assignable to // parameter of type 'StatusCode' message: "Welcome, user " + req.params.userId // ^^^^^^ // Property 'userId' does not exist on type // '{ userID: string; }'. Did you mean 'userID'? }); } })

Und das alles, bevor wir unseren Code tatsächlich ausführen! Server im Express-Stil sind ein perfektes Beispiel für die dynamische Natur von JavaScript. Abhängig von der Methode, die Sie aufrufen, der Zeichenfolge, die Sie als erstes Argument übergeben, ändern sich viele Verhaltensweisen innerhalb des Rückrufs. Nehmen Sie ein anderes Beispiel und alle Ihre Typen sehen völlig anders aus.

Aber mit ein paar gut definierten Typen können wir dieses dynamische Verhalten beim Bearbeiten unseres Codes abfangen. Zur Kompilierzeit mit statischen Typen, nicht zur Laufzeit, wenn es boomt!

Und das ist die Stärke von TypeScript. Ein statisches Typsystem, das versucht, das gesamte dynamische JavaScript-Verhalten, das wir alle so gut kennen, zu formalisieren. Wenn Sie das gerade erstellte Beispiel ausprobieren möchten, gehen Sie zum TypeScript Playground und spielen Sie damit herum.


TypeScript in 50 Lektionen von Stefan Baumgartner In diesem Artikel haben wir viele Konzepte angesprochen. Wenn Sie mehr wissen möchten, sehen Sie sich TypeScript in 50 Lektionen an, wo Sie in kleinen, leicht verdaulichen Lektionen eine sanfte Einführung in das Typsystem erhalten. E-Book-Versionen sind sofort verfügbar, und das gedruckte Buch ist eine großartige Referenz für Ihre Programmierbibliothek.