Dynamisches statisches Tippen in TypeScript
Veröffentlicht: 2022-03-10JavaScript 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:
- 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 sollteuserID
einem ParameteruserID
werden, der, nun ja, die ID des Benutzers enthält! - Das
response
oderreply
.
Hier wollen wir eine richtige Antwort vom Server an den Client vorbereiten. Wir möchten korrekte Statuscodes (methodstatus
) 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?
- 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 kannreq.method
nicht POST sein. Wir würden also niemals eine Antwort senden, was zu unerwarteten Zeitüberschreitungen führen könnte. - Toll, dass wir explizit einen Statuscode versenden!
20
ist jedoch kein gültiger Statuscode. Kunden verstehen möglicherweise nicht, was hier passiert. - 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
nichtuserId
. 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.
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 TypServerRequest
ist -
reply
, die vom TypServerReply
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:
-
ServerRequest
wird zu einem generischen Typ, wie durch die spitzen Klammern angegeben - Wir definieren einen generischen Parameter namens
Met
, der eine Teilmenge des TypsMethods
ist - 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:
- 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. - 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.
- 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! - 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:
- 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 mitRest
aufrufen. Wenn wir beispielsweise die Route"/api/users/:userID/orders/:orderID"
anParseRouteParams
übergeben, leiten wir"userID"
inP
und"orders/:orderID"
inRest
ab. Wir nennen den gleichen Typ mitRest
- 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. - 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:
- Wir können nur richtige numerische Statuscodes an
res.status()
-
req.method
ist eine von vier möglichen Zeichenfolgen, und wenn wirapp.get
verwenden, wissen wir, dass es nur"GET"
ist. - 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.
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.