Dynamiczne pisanie statyczne w TypeScript

Opublikowany: 2022-03-10
Krótkie podsumowanie ↬ W tym artykule przyjrzymy się niektórym bardziej zaawansowanym funkcjom języka TypeScript, takim jak typy sum, typy warunkowe, typy literałów szablonów i typy generyczne. Chcemy sformalizować najbardziej dynamiczne zachowanie JavaScript w taki sposób, abyśmy mogli wyłapać większość błędów, zanim się pojawią. Stosujemy kilka wniosków ze wszystkich rozdziałów TypeScript w 50 lekcjach, książce, którą opublikowaliśmy tutaj w Smashing Magazine pod koniec 2020 roku. Jeśli chcesz dowiedzieć się więcej, koniecznie sprawdź to!

JavaScript jest z natury dynamicznym językiem programowania. Jako programiści możemy wyrazić wiele przy niewielkim wysiłku, a język i jego środowisko wykonawcze określają, co zamierzaliśmy zrobić. To właśnie sprawia, że ​​JavaScript jest tak popularny wśród początkujących i sprawia, że ​​doświadczeni programiści są produktywni! Jest jednak zastrzeżenie: musimy być czujni! Błędy, literówki, prawidłowe zachowanie programu: wiele z nich dzieje się w naszych głowach!

Spójrz na poniższy przykład.

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

Mamy serwer https://expressjs.com/-style, który pozwala nam zdefiniować trasę (lub ścieżkę) i wykonuje wywołanie zwrotne, jeśli zażądano adresu URL.

Callback przyjmuje dwa argumenty:

  1. Obiekt request .
    Tutaj otrzymujemy informacje o użytej metodzie HTTP (np. GET, POST, PUT, DELETE) i dodatkowych parametrach, które wchodzą. W tym przykładzie userID powinien być zmapowany na parametr userID , który, cóż, zawiera ID użytkownika!
  2. response lub obiekt reply .
    Tutaj chcemy przygotować odpowiednią odpowiedź z serwera do klienta. Chcemy wysyłać poprawne kody statusu ( status metody) i przesyłać wyjście JSON przez przewód.

To, co widzimy w tym przykładzie, jest mocno uproszczone, ale daje dobre wyobrażenie o tym, co zamierzamy. Powyższy przykład jest również pełen błędów! Spójrz:

 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 */ }); } })

Och! Trzy linijki kodu implementacyjnego i trzy błędy? Co się stało?

  1. Pierwszy błąd jest dopracowany. Chociaż mówimy naszej aplikacji, że chcemy słuchać żądań GET (stąd app.get ), robimy coś tylko wtedy, gdy metodą żądania jest POST . W tym konkretnym momencie naszej aplikacji req.method nie może być POST . Dlatego nigdy nie wyślemy żadnej odpowiedzi, co może prowadzić do nieoczekiwanego przekroczenia limitu czasu.
  2. Świetnie, że wyraźnie wysyłamy kod statusu! 20 nie jest jednak prawidłowym kodem statusu. Klienci mogą nie rozumieć, co się tutaj dzieje.
  3. To jest odpowiedź, którą chcemy odesłać. Mamy dostęp do przeanalizowanych argumentów, ale mamy średnią literówkę. To userID , a nie userId użytkownika . Wszyscy nasi użytkownicy zostaliby powitani słowami „Witaj, niezdefiniowany użytkownik!”. Coś, co na pewno widziałeś na wolności!

I takie rzeczy się zdarzają! Zwłaszcza w JavaScript. Zyskujemy wyrazistość – ani razu nie musieliśmy przejmować się typami – ale musimy zwracać baczną uwagę na to, co robimy.

Jest to również miejsce, w którym JavaScript otrzymuje wiele reakcji ze strony programistów, którzy nie są przyzwyczajeni do dynamicznych języków programowania. Zwykle mają kompilatory, które wskazują im możliwe problemy i od razu wyłapują błędy. Mogą wyglądać na nadętych, gdy marszczą brwi na ilość dodatkowej pracy, którą musisz wykonać w swojej głowie, aby upewnić się, że wszystko działa dobrze. Mogą nawet powiedzieć, że JavaScript nie ma typów. Co nie jest prawdą.

Anders Hejlsberg, główny architekt TypeScript, powiedział w swoim przemówieniu do MS Build 2017, że „ to nie jest tak, że JavaScript nie ma systemu typów. Po prostu nie ma możliwości sformalizowania tego ”.

I to jest głównym celem TypeScript. TypeScript chce lepiej zrozumieć Twój kod JavaScript niż Ty. A gdy TypeScript nie może zrozumieć, co masz na myśli, możesz pomóc, podając dodatkowe informacje o typie.

Więcej po skoku! Kontynuuj czytanie poniżej ↓

Podstawowe pisanie

I to właśnie zamierzamy teraz zrobić. Weźmy metodę get z naszego serwera w stylu Express i dodajmy wystarczającą ilość informacji o typie, abyśmy mogli wykluczyć jak najwięcej kategorii błędów.

Zaczniemy od podstawowych informacji o typie. Mamy obiekt app , który wskazuje na funkcję get . Funkcja get pobiera path , który jest ciągiem znaków i wywołaniem zwrotnym.

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

Podczas gdy string jest podstawowym, tak zwanym typem pierwotnym , CallbackFn jest typem złożonym , który musimy wyraźnie zdefiniować.

CallbackFn to typ funkcji, który przyjmuje dwa argumenty:

  • req , który jest typu ServerRequest
  • reply typu ServerReply

CallbackFn zwraca void .

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

ServerRequest jest dość złożonym obiektem w większości frameworków. Wykonujemy uproszczoną wersję do celów demonstracyjnych. Przekazujemy ciąg method dla "GET" , "POST" , "PUT" , "DELETE" , itp. Posiada również rekord params . Rekordy to obiekty, które kojarzą zestaw kluczy z zestawem właściwości. Na razie chcemy zezwolić na odwzorowanie każdego klucza string na właściwość string . Refaktoryzujemy to później.

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

W przypadku ServerReply przedstawiamy kilka funkcji, wiedząc, że prawdziwy obiekt ServerReply ma znacznie więcej. Funkcja send pobiera opcjonalny argument z danymi, które chcemy wysłać. I mamy możliwość ustawienia kodu statusu za pomocą funkcji status .

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

To już coś i możemy wykluczyć kilka błędów:

 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 } })

Ale nadal możemy wysyłać błędne kody statusu (możliwa jest dowolna liczba) i nie mamy pojęcia o możliwych metodach HTTP (możliwy jest dowolny ciąg). Dopracujmy nasze typy.

Mniejsze zestawy

Typy pierwotne można zobaczyć jako zbiór wszystkich możliwych wartości tej określonej kategorii. Na przykład string zawiera wszystkie możliwe ciągi, które można wyrazić w JavaScript, number zawiera wszystkie możliwe liczby z podwójną dokładnością zmiennoprzecinkową. boolean zawiera wszystkie możliwe wartości logiczne, które są true i false .

TypeScript umożliwia doprecyzowanie tych zestawów do mniejszych podzbiorów. Na przykład możemy utworzyć typ Method , który zawiera wszystkie możliwe ciągi, jakie możemy otrzymać dla metod HTTP:

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

Method to mniejszy zestaw większego zestawu string . Method jest typem unii typów dosłownych. Typ literału to najmniejsza jednostka danego zestawu. Dosłowny ciąg. Dosłowna liczba. Nie ma dwuznaczności. To po prostu "GET" . Łączysz je z innymi typami dosłownymi, tworząc podzbiór dowolnych większych typów. Możesz również utworzyć podzbiór z typami literałowymi typu string i number lub różnymi złożonymi typami obiektów. Istnieje wiele możliwości łączenia i umieszczania typów dosłownych w uniach.

Ma to natychmiastowy wpływ na wywołanie zwrotne naszego serwera. Nagle możemy rozróżnić te cztery metody (lub więcej, jeśli to konieczne) i możemy wyczerpać wszystkie możliwości w kodzie. TypeScript poprowadzi nas:

 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; } });

Przy każdym oświadczeniu case , TypeScript może dostarczyć informacji o dostępnych opcjach. Wypróbuj sam. Jeśli wyczerpałeś wszystkie opcje, TypeScript powie ci w default gałęzi, że to się never stanie. Jest to dosłownie typ never , co oznacza, że ​​prawdopodobnie osiągnąłeś stan błędu, który musisz obsłużyć.

To o jedną kategorię błędów mniej. Wiemy już dokładnie, jakie możliwe metody HTTP są dostępne.

Możemy zrobić to samo dla kodów stanu HTTP, definiując podzbiór prawidłowych liczb, które może przyjąć statusCode :

 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 jest ponownie typem unii. I tym samym wykluczamy kolejną kategorię błędów. Nagle kod taki nie działa:

 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' } })
A nasze oprogramowanie staje się dużo bezpieczniejsze! Ale możemy więcej!

Wprowadź Generyki

Kiedy definiujemy trasę za pomocą app.get , domyślnie wiemy, że jedyną możliwą metodą HTTP jest "GET" . Ale przy naszych definicjach typów nadal musimy sprawdzać wszystkie możliwe części unii.

Typ dla CallbackFn jest poprawny, ponieważ moglibyśmy zdefiniować funkcje zwrotne dla wszystkich możliwych metod HTTP, ale jeśli jawnie app.get , byłoby miło zaoszczędzić kilka dodatkowych kroków, które są potrzebne tylko do zachowania zgodności z typami.

Generyki TypeScript mogą pomóc! Generics to jedna z głównych funkcji TypeScript, która pozwala uzyskać najbardziej dynamiczne zachowanie z typów statycznych. W TypeScript w 50 lekcjach ostatnie trzy rozdziały spędzamy na zagłębianiu się we wszystkie zawiłości generyków i ich unikalnej funkcjonalności.

Musisz teraz wiedzieć, że chcemy zdefiniować ServerRequest w taki sposób, abyśmy mogli określić część Methods zamiast całego zestawu. W tym celu używamy ogólnej składni, w której możemy definiować parametry tak, jak robimy to z funkcjami:

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

Oto co się dzieje:

  1. ServerRequest staje się typem ogólnym, jak wskazują nawiasy kątowe
  2. Definiujemy ogólny parametr o nazwie Met , który jest podzbiorem typu Methods
  3. Używamy tego ogólnego parametru jako zmiennej ogólnej do zdefiniowania metody.

Zachęcam również do zapoznania się z moim artykułem na temat nazewnictwa parametrów generycznych.

Dzięki tej zmianie możemy określić różne ServerRequest bez duplikowania rzeczy:

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

Ponieważ zmieniliśmy interfejs ServerRequest , musimy wprowadzić zmiany we wszystkich naszych innych typach, które używają ServerRequest , takich jak CallbackFn i funkcja get :

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

Za pomocą funkcji get przekazujemy rzeczywisty argument do naszego typu generycznego. Wiemy, że nie będzie to tylko podzbiór Methods , wiemy dokładnie, z którym podzbiorem mamy do czynienia.

Teraz, gdy używamy app.get , mamy tylko możliwą wartość dla req.method :

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

Dzięki temu nie zakładamy, że metody HTTP, takie jak "POST" lub podobne, są dostępne podczas tworzenia wywołania zwrotnego app.get . Dokładnie wiemy, z czym mamy do czynienia w tym momencie, więc odzwierciedlmy to w naszych typach.

Zrobiliśmy już wiele, aby upewnić się, że request.method jest poprawnie wpisany i reprezentuje rzeczywisty stan rzeczy. Jedną z fajnych korzyści, jakie uzyskujemy dzięki podstawieniu typu unii Methods , jest to, że możemy utworzyć funkcję wywołania zwrotnego ogólnego przeznaczenia poza app.get , która jest bezpieczna dla typów:

 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

Wpisywanie parametrów

To, czego jeszcze nie dotknęliśmy, to wpisanie obiektu params . Do tej pory otrzymujemy rekord, który umożliwia dostęp do każdego klucza string . Naszym zadaniem jest teraz doprecyzowanie tego!

Robimy to, dodając kolejną zmienną generyczną. Jeden dla metod, jeden dla możliwych kluczy w naszym Record :

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

Zmienna typu ogólnego Par może być podzbiorem typu string , a wartością domyślną jest każdy ciąg. Dzięki temu możemy powiedzieć ServerRequest , jakich kluczy oczekujemy:

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

Dodajmy nowy argument do naszej funkcji get oraz typ CallbackFn , abyśmy mogli ustawić żądane parametry:

 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;

Jeśli nie ustawimy Par jawnie, typ działa tak, jak przywykliśmy, ponieważ Par domyślnie przyjmuje wartość string . Jeśli jednak to ustawimy, nagle otrzymamy poprawną definicję obiektu req.params !

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

To wspaniale! Jest jednak jedna mała rzecz, którą można poprawić. Nadal możemy przekazać każdy ciąg do argumentu path w app.get . Czy nie byłoby lepiej, gdybyśmy mogli również tam odzwierciedlić Par ?

Możemy! Wraz z wydaniem wersji 4.1, TypeScript może tworzyć typy literałów szablonu . Pod względem składniowym działają one tak samo jak literały szablonu ciągu, ale na poziomie typu. Tam, gdzie byliśmy w stanie podzielić string na podzbiory za pomocą typów literałów ciągów (tak jak zrobiliśmy to w przypadku metod), typy literałów szablonów pozwalają nam uwzględnić całe spektrum ciągów.

Stwórzmy typ o nazwie IncludesRouteParams , w którym chcemy się upewnić, że Par jest poprawnie uwzględniony w stylu Express poprzez dodanie dwukropka przed nazwą parametru:

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

Typ ogólny IncludesRouteParams przyjmuje jeden argument, który jest podzbiorem string . Tworzy typ unii dwóch literałów szablonu:

  1. Pierwszy literał szablonu zaczyna się od dowolnego string , a następnie zawiera znak / , po którym następuje znak : , po którym następuje nazwa parametru. Dzięki temu mamy pewność, że przechwycimy wszystkie przypadki, w których parametr znajduje się na końcu ciągu trasy.
  2. Drugi literał szablonu zaczyna się od dowolnego string , po którym następuje ten sam wzorzec / , : i nazwa parametru. Następnie mamy kolejny znak / , po którym następuje dowolny ciąg. Ta gałąź typu union zapewnia, że ​​przechwycimy wszystkie przypadki, w których parametr znajduje się gdzieś w obrębie trasy.

Oto jak działa funkcja IncludesRouteParams z nazwą parametru userID w różnych przypadkach testowych:

 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" //

Dołączmy nasz nowy typ narzędzia do deklaracji funkcji get .

 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! } );

Świetnie! Dostajemy kolejny mechanizm bezpieczeństwa, aby nie przegapić dodawania parametrów do rzeczywistej trasy! Jak potężny.

Wiązania ogólne

Ale wiecie co, nadal nie jestem z tego zadowolony. Jest kilka problemów związanych z tym podejściem, które stają się oczywiste, gdy Twoje trasy stają się nieco bardziej złożone.

  1. Pierwszym problemem, jaki mam, jest to, że musimy jawnie określić nasze parametry w parametrze typu ogólnego. Musimy powiązać Par z "userID" , chociaż i tak określilibyśmy to w argumencie path funkcji. To nie jest JavaScript-y!
  2. To podejście obsługuje tylko jeden parametr trasy. W momencie dodawania unii, np. "userID" | "orderId" "userID" | "orderId" sprawdzanie w razie niepowodzenia spełnia tylko jeden z tych argumentów. Tak działają zestawy. Może to być jedno lub drugie.

Musi być lepszy sposób. I jest. W przeciwnym razie ten artykuł zakończyłby się bardzo gorzkim tonem.

Odwróćmy kolejność! Nie próbujmy definiować parametrów trasy w zmiennej typu ogólnego, ale raczej wyodrębnijmy zmienne ze path , którą przekazujemy jako pierwszy argument app.get .

Aby dostać się do rzeczywistej wartości, musimy zobaczyć, jak działa generyczne wiązanie w TypeScript. Weźmy na przykład tę funkcję identity :

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

Może to być najnudniejsza ogólna funkcja, jaką kiedykolwiek widziałeś, ale doskonale ilustruje jeden punkt. identity przyjmuje jeden argument i ponownie zwraca te same dane wejściowe. Typ jest typem ogólnym T i zwraca również ten sam typ.

Teraz możemy powiązać T z string , na przykład:

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

To jawnie generyczne wiązanie gwarantuje, że do identity przekazujemy tylko strings , a ponieważ jawnie wiążemy, zwracanym typem jest również string . Jeśli zapomnimy związać, dzieje się coś ciekawego:

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

W takim przypadku TypeScript wnioskuje typ z argumentu, który przekazujesz, i wiąże T z typem literału ciągu "yes" . Jest to świetny sposób na przekonwertowanie argumentu funkcji na typ literałowy, którego następnie używamy w naszych innych typach ogólnych.

Zróbmy to, dostosowując app.get .

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

Usuwamy typ ogólny Par i dodajemy Path . Path może być podzbiorem dowolnego string . Ustawiamy path na ten typ ogólny Path , co oznacza, że ​​w momencie przekazania parametru do get , przechwytujemy jego typ literału ciągu. Przekazujemy Path do nowego typu generycznego ParseRouteParams , którego jeszcze nie stworzyliśmy.

Popracujmy nad ParseRouteParams . Tutaj ponownie zmieniamy kolejność wydarzeń. Zamiast przekazywać żądane parametry trasy do ogólnych, aby upewnić się, że ścieżka jest w porządku, przekazujemy ścieżkę trasy i wyodrębniamy możliwe parametry trasy. W tym celu musimy stworzyć typ warunkowy.

Typy warunkowe i rekurencyjne typy literałów szablonów

Typy warunkowe są składniowo podobne do operatora trójargumentowego w JavaScript. Sprawdzasz warunek, a jeśli warunek jest spełniony, zwracasz gałąź A, w przeciwnym razie zwracasz gałąź B. Na przykład:

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

Tutaj sprawdzamy, czy Rte jest podzbiorem każdej ścieżki, która kończy się parametrem na końcu stylu Express (z poprzedzającym "/:" ). Jeśli tak, wywnioskujemy ten ciąg. Co oznacza, że ​​przechwytujemy jego zawartość do nowej zmiennej. Jeśli warunek jest spełniony, zwracamy nowo wyodrębniony ciąg, w przeciwnym razie zwracamy nigdy, jak w: „Brak parametrów trasy”,

Jeśli to wypróbujemy, otrzymamy coś takiego:

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

Świetnie, to już jest znacznie lepsze niż wcześniej. Teraz chcemy przechwycić wszystkie inne możliwe parametry. W tym celu musimy dodać kolejny warunek:

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

Nasz typ warunkowy działa teraz w następujący sposób:

  1. W pierwszym warunku sprawdzamy, czy gdzieś pomiędzy trasą znajduje się parametr trasy. Jeśli tak, wyodrębniamy zarówno parametr trasy, jak i wszystko inne, co nastąpi po nim. Zwracamy nowo znaleziony parametr trasy P w unii, w której ten sam typ ogólny wywołujemy rekursywnie z Rest . Na przykład, jeśli przekażemy trasę "/api/users/:userID/orders/:orderID" do ParseRouteParams , wnioskujemy "userID" do P , a "orders/:orderID" do Rest . Nazywamy ten sam typ z Rest
  2. Tutaj pojawia się drugi warunek. Tutaj sprawdzamy, czy na końcu znajduje się typ. Tak jest w przypadku "orders/:orderID" . "orderID" i zwracamy ten typ literału.
  3. Jeśli nie ma już parametrów trasy, zwracamy nigdy.

Dan Vanderkam pokazuje podobny i bardziej rozbudowany typ dla ParseRouteParams , ale ten, który widzisz powyżej, również powinien działać. Jeśli wypróbujemy nasze nowo zaadaptowane ParseRouteParams , otrzymamy coś takiego:

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

Zastosujmy ten nowy typ i zobaczmy, jak wygląda nasze ostateczne wykorzystanie app.get .

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

Wow. To po prostu wygląda jak kod JavaScript, który mieliśmy na początku!

Typy statyczne dla zachowania dynamicznego

Typy, które właśnie stworzyliśmy dla jednej funkcji app.get , zapewniają, że wykluczymy mnóstwo możliwych błędów:

  1. Do res.status() możemy przekazać tylko odpowiednie numeryczne kody statusu
  2. req.method to jeden z czterech możliwych ciągów, a kiedy używamy app.get , wiemy, że jest to tylko "GET"
  3. Możemy parsować parametry trasy i upewnić się, że nie ma żadnych literówek w naszym wywołaniu zwrotnym

Jeśli spojrzymy na przykład z początku tego artykułu, otrzymamy następujące komunikaty o błędach:

 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'? }); } })

A wszystko to zanim faktycznie uruchomimy nasz kod! Serwery w stylu Express są doskonałym przykładem dynamicznej natury JavaScript. W zależności od wywołanej metody, ciągu, który przekazujesz jako pierwszy argument, wewnątrz wywołania zwrotnego zmienia się wiele zachowań. Weź inny przykład, a wszystkie twoje typy wyglądają zupełnie inaczej.

Ale dzięki kilku dobrze zdefiniowanym typom możemy przechwycić to dynamiczne zachowanie podczas edycji naszego kodu. W czasie kompilacji ze statycznymi typami, a nie w czasie wykonywania, gdy wszystko idzie w górę!

I to jest siła TypeScript. Statyczny system typów, który próbuje sformalizować wszystkie dynamiczne zachowania JavaScriptu, które wszyscy tak dobrze znamy. Jeśli chcesz wypróbować właśnie utworzony przykład, przejdź do placu zabaw TypeScript i pobaw się nim.


TypeScript w 50 lekcjach Stefana Baumgartnera W tym artykule poruszyliśmy wiele koncepcji. Jeśli chcesz dowiedzieć się więcej, zapoznaj się z TypeScript w 50 lekcjach, gdzie otrzymasz łagodne wprowadzenie do systemu czcionek w postaci krótkich, łatwo przyswajalnych lekcji. Wersje e-booków są dostępne natychmiast, a książka drukowana będzie doskonałym źródłem informacji dla Twojej biblioteki kodowania.