Tastare dinamică statică în TypeScript
Publicat: 2022-03-10JavaScript este un limbaj de programare inerent dinamic. Noi, în calitate de dezvoltatori, putem exprima multe cu puțin efort, iar limbajul și timpul său de execuție își dă seama ce intenționăm să facem. Acesta este ceea ce face ca JavaScript să fie atât de popular pentru începători și care face ca dezvoltatorii experimentați să fie productivi! Există totuși un avertisment: trebuie să fim atenți! Greșeli, greșeli de scriere, comportamentul corect al programului: multe dintre acestea se întâmplă în capul nostru!
Aruncă o privire la următorul exemplu.
app.get("/api/users/:userID", function(req, res) { if (req.method === "POST") { res.status(20).send({ message: "Got you, user " + req.params.userId }); } })
Avem un server https://expressjs.com/-style care ne permite să definim o rută (sau cale) și execută un callback dacă adresa URL este solicitată.
Callback-ul are două argumente:
- Obiectul
request
.
Aici obținem informații despre metoda HTTP utilizată (de exemplu, GET, POST, PUT, DELETE) și parametri suplimentari care vin. În acest exemplu,userID
ar trebui să fie mapat la un parametruuserID
care, bine, conține ID-ul utilizatorului! - Obiectul de
response
saureply
.
Aici dorim să pregătim un răspuns adecvat de la server către client. Dorim să trimitem coduri de stare corecte (status
metodei) și să trimitem ieșire JSON prin cablu.
Ceea ce vedem în acest exemplu este mult simplificat, dar oferă o idee bună despre ce facem. Exemplul de mai sus este, de asemenea, plin de erori! Uită-te:
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! Trei linii de cod de implementare și trei erori? Ce s-a întâmplat?
- Prima eroare este nuanțată. În timp ce spunem aplicației noastre că vrem să ascultăm cererile GET (de aici
app.get
), facem ceva doar dacă metoda de solicitare este POST . În acest moment al aplicației noastre,req.method
nu poate fi POST . Așa că nu vom trimite niciodată niciun răspuns, ceea ce ar putea duce la expirări neașteptate. - Foarte bine că trimitem în mod explicit un cod de stare!
20
nu este un cod de stare valid, totuși. Clienții s-ar putea să nu înțeleagă ce se întâmplă aici. - Acesta este răspunsul pe care vrem să-l trimitem înapoi. Accesăm argumentele analizate, dar avem o greșeală de tipar. Este
userID
nuuserId
. Toți utilizatorii noștri vor fi întâmpinați cu „Bun venit, utilizator nedefinit!”. Ceva pe care cu siguranță ai văzut în sălbăticie!
Și se întâmplă astfel de lucruri! Mai ales în JavaScript. Dobândim expresivitate – nu o dată a trebuit să ne deranjez în privința tipurilor – dar trebuie să fim foarte atenți la ceea ce facem.
Aici este și locul în care JavaScript primește o mulțime de reacții de la programatori care nu sunt obișnuiți cu limbaje de programare dinamică. De obicei, au compilatoare care le indică posibile probleme și detectează erorile din timp. S-ar putea să devină aiurea când se încruntă la cantitatea de muncă suplimentară pe care trebuie să o faci în capul tău pentru a te asigura că totul funcționează corect. S-ar putea chiar să vă spună că JavaScript nu are tipuri. Ceea ce nu este adevărat.
Anders Hejlsberg, arhitectul principal al TypeScript, a spus în discursul său MS Build 2017 că „ nu înseamnă că JavaScript nu are un sistem de tipări. Pur și simplu nu există nicio modalitate de a o oficializa ”.
Și acesta este scopul principal al TypeScript. TypeScript vrea să înțeleagă codul JavaScript mai bine decât tine. Și acolo unde TypeScript nu poate înțelege ce vrei să spui, poți ajuta oferind informații suplimentare despre tip.
Tastarea de bază
Și asta este ceea ce vom face chiar acum. Să luăm metoda get
de pe serverul nostru în stil Express și să adăugăm suficiente informații de tip, astfel încât să putem exclude cât mai multe categorii de erori posibil.
Începem cu câteva informații de tip de bază. Avem un obiect app
care indică o funcție get
. Funcția get
ia path
, care este un șir și un callback.
const app = { get, /* post, put, delete, ... to come! */ }; function get(path: string, callback: CallbackFn) { // to be implemented --> not important right now }
În timp ce string
este un tip de bază, așa-numitul primitiv , CallbackFn
este un tip compus pe care trebuie să-l definim în mod explicit.
CallbackFn
este un tip de funcție care ia două argumente:
-
req
, care este de tipServerRequest
-
reply
care este de tipServerReply
CallbackFn
returnează void
.
type CallbackFn = (req: ServerRequest, reply: ServerReply) => void;
ServerRequest
este un obiect destul de complex în majoritatea cadrelor. Facem o versiune simplificată în scop demonstrativ. Trecem un șir de method
, pentru "GET"
, "POST"
, "PUT"
, "DELETE"
, etc. Are și o înregistrare de params
. Înregistrările sunt obiecte care asociază un set de chei cu un set de proprietăți. Pentru moment, dorim să permitem ca fiecare cheie string
să fie mapată la o proprietate string
. Pe acesta îl refactorăm mai târziu.
type ServerRequest = { method: string; params: Record<string, string>; };
Pentru ServerReply
, prezentăm câteva funcții, știind că un obiect ServerReply
real are mult mai multe. O funcție de send
ia un argument opțional cu datele pe care dorim să le trimitem. Și avem posibilitatea de a seta un cod de status
cu funcția de stare.
type ServerReply = { send: (obj?: any) => void; status: (statusCode: number) => ServerReply; };
Este deja ceva și putem exclude câteva erori:
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 } })
Dar încă putem trimite coduri de stare greșite (orice număr este posibil) și nu avem nicio idee despre posibilele metode HTTP (orice șir este posibil). Să ne rafinăm tipurile.
Seturi mai mici
Puteți vedea tipurile primitive ca un set de toate valorile posibile ale acelei categorii. De exemplu, string
include toate șirurile posibile care pot fi exprimate în JavaScript, number
include toate numerele posibile cu precizie dublă. boolean
include toate valorile booleene posibile, care sunt true
și false
.
TypeScript vă permite să rafinați acele seturi la subseturi mai mici. De exemplu, putem crea un tip Method
care include toate șirurile posibile pe care le putem primi pentru metodele HTTP:
type Methods= "GET" | "POST" | "PUT" | "DELETE"; type ServerRequest = { method: Methods; params: Record<string, string>; };
Method
este un set mai mic din setul de string
mai mare. Method
este un tip de uniune de tipuri literale. Un tip literal este cea mai mică unitate dintr-o mulțime dată. Un șir literal. Un număr literal. Nu există ambiguitate. Este doar "GET"
. Le puneți într-o uniune cu alte tipuri literale, creând un subset de tipuri mai mari pe care le aveți. De asemenea, puteți face un subset cu tipuri literale atât de string
, cât și de number
sau diferite tipuri de obiecte compuse. Există o mulțime de posibilități de a combina și de a pune tipuri literale în uniuni.
Acest lucru are un efect imediat asupra apelului înapoi pe serverul nostru. Dintr-o dată, putem diferenția între cele patru metode (sau mai multe dacă este necesar) și putem epuiza toate posibilitățile din cod. TypeScript ne va ghida:
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; } });
Cu fiecare declarație de case
pe care o faceți, TypeScript vă poate oferi informații despre opțiunile disponibile. Încercați-l singur. Dacă ați epuizat toate opțiunile, TypeScript vă va spune în ramura dvs. default
că acest lucru nu se poate întâmpla never
. Acesta este literalmente tipul never
, ceea ce înseamnă că probabil ați ajuns la o stare de eroare pe care trebuie să o gestionați.
Aceasta este o categorie de erori mai puțin. Știm acum exact ce metode HTTP posibile sunt disponibile.
Putem face același lucru pentru codurile de stare HTTP, prin definirea unui subset de numere valide pe care statusCode
le poate lua:
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; };
Tip StatusCode
este din nou un tip de uniune. Și cu asta, excludem o altă categorie de erori. Dintr-o dată, un astfel de cod eșuează:
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' } })
Iar software-ul nostru devine mult mai sigur! Dar putem face mai mult!Introduceți generice
Când definim o rută cu app.get
, știm implicit că singura metodă HTTP posibilă este "GET"
. Dar, cu definițiile noastre de tip, mai trebuie să verificăm toate părțile posibile ale uniunii.
Tipul pentru CallbackFn
este corect, deoarece am putea defini funcții de apel invers pentru toate metodele HTTP posibile, dar dacă apelăm în mod explicit app.get
, ar fi bine să salvăm câțiva pași suplimentari care sunt necesari doar pentru a respecta tastările.
Genericele TypeScript vă pot ajuta! Genericurile sunt una dintre caracteristicile majore din TypeScript care vă permit să obțineți cel mai dinamic comportament din tipurile statice. În TypeScript în 50 de lecții, petrecem ultimele trei capitole cercetând toate complexitățile genericelor și funcționalitatea lor unică.
Ceea ce trebuie să știți acum este că vrem să definim ServerRequest
într-un mod în care să putem specifica o parte din Methods
în loc de întregul set. Pentru asta, folosim sintaxa generică în care putem defini parametrii așa cum am face cu funcțiile:
type ServerRequest<Met extends Methods> = { method: Met; params: Record<string, string>; };
Asta se intampla:
-
ServerRequest
devine un tip generic, așa cum este indicat de parantezele unghiulare - Definim un parametru generic numit
Met
, care este un subset de tipMethods
- Folosim acest parametru generic ca variabilă generică pentru a defini metoda.
De asemenea, vă încurajez să consultați articolul meu despre denumirea parametrilor generici.
Cu această modificare, putem specifica diferite ServerRequest
fără a duplica lucruri:
type OnlyGET = ServerRequest<"GET">; type OnlyPOST = ServerRequest<"POST">; type POSTorPUT = ServerRquest<"POST" | "PUT">;
Deoarece am schimbat interfața ServerRequest
, trebuie să facem modificări tuturor celorlalte tipuri care folosesc ServerRequest
, cum ar fi CallbackFn
și funcția get
:
type CallbackFn<Met extends Methods> = ( req: ServerRequest<Met>, reply: ServerReply ) => void; function get(path: string, callback: CallbackFn<"GET">) { // to be implemented }
Cu funcția get
, trecem un argument real tipului nostru generic. Știm că acesta nu va fi doar un subset de Methods
, știm exact cu ce subset avem de-a face.
Acum, când folosim app.get
, avem doar o valoare posibilă pentru req.method
:
app.get("/api/users/:userID", function (req, res) { req.method; // can only be get });
Acest lucru ne asigură că nu presupunem că metode HTTP precum "POST"
sau similare sunt disponibile atunci când creăm un apel invers app.get
. Știm exact cu ce avem de-a face în acest moment, așa că să reflectăm asta în tipurile noastre.
Am făcut deja multe pentru a ne asigura că request.method
este scrisă în mod rezonabil și reprezintă starea reală a lucrurilor. Un beneficiu frumos pe care îl obținem prin subsetarea tipului de unire Methods
este că putem crea o funcție de apel invers pentru uz general în afara app.get
, care este sigură pentru tip:
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
Tastarea Params
Ceea ce nu ne-am atins încă este să tastăm obiectul params
. Până acum, obținem o înregistrare care permite accesarea fiecărei chei string
. Este sarcina noastră acum să facem asta un pic mai specific!
Facem asta adăugând o altă variabilă generică. Una pentru metode, una pentru cheile posibile din Record
noastră:
type ServerRequest<Met extends Methods, Par extends string = string> = { method: Met; params: Record<Par, string>; };
Variabila de tip generic Par
poate fi un subset de tip string
, iar valoarea implicită este fiecare șir. Cu asta, putem spune ServerRequest
ce chei așteptăm:
// request.method = "GET" // request.params = { // userID: string // } type WithUserID = ServerRequest<"GET", "userID">
Să adăugăm noul argument la funcția noastră get
și la tipul CallbackFn
, astfel încât să putem seta parametrii solicitați:
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;
Dacă nu setăm Par
în mod explicit, tipul funcționează așa cum ne-am obișnuit, deoarece Par
implicit este string
. Totuși, dacă îl setăm, avem dintr-o dată o definiție adecvată pentru obiectul req.params
!
app.get<"userID">("/api/users/:userID", function (req, res) { req.params.userID; // Works!! req.params.anythingElse; // doesn't work!! });
Grozav! Există totuși un lucru mic care poate fi îmbunătățit. Încă putem trece fiecare șir la argumentul path
al app.get
. N-ar fi mai bine dacă am putea reflecta și Par
acolo?
Putem! Odată cu lansarea versiunii 4.1, TypeScript este capabil să creeze tipuri literale de șablon . Sintactic, ele funcționează la fel ca șirurile de caractere șablon, dar la nivel de tip. Acolo unde am putut împărți string
setat în subseturi cu tipuri literale de șir (cum am făcut cu Metode), tipurile literale de șablon ne permit să includem un întreg spectru de șiruri.
Să creăm un tip numit IncludesRouteParams
, unde vrem să ne asigurăm că Par
este inclus în mod corespunzător în modul în stil Express de a adăuga două puncte în fața numelui parametrului:
type IncludesRouteParams<Par extends string> = | `${string}/:${Par}` | `${string}/:${Par}/${string}`;
Tipul generic IncludesRouteParams
ia un argument, care este un subset de string
. Se creează un tip de unire a două literale șablon:
- Primul literal șablon începe cu orice
string
, apoi include un caracter/
urmat de un caracter:
, urmat de numele parametrului. Acest lucru ne asigură că prindem toate cazurile în care parametrul se află la sfârșitul șirului de rută. - Al doilea literal șablon începe cu orice
string
, urmat de același model de/
,:
și numele parametrului. Apoi avem un alt caracter/
, urmat de orice șir. Această ramură de tip unire se asigură că prindem toate cazurile în care parametrul se află undeva într-o rută.
Acesta este modul în care IncludesRouteParams
cu numele parametrului userID
se comportă cu diferite cazuri de testare:
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" //
Să includem noul nostru tip de utilitar în declarația funcției 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! } );
Grozav! Avem un alt mecanism de siguranță pentru a ne asigura că nu pierdem adăugarea parametrilor la traseul propriu-zis! Cât de puternic.
Legături generice
Dar ghici ce, încă nu sunt mulțumit de asta. Există câteva probleme cu această abordare care devin evidente în momentul în care rutele dvs. devin puțin mai complexe.
- Prima problemă pe care o am este că trebuie să ne precizăm în mod explicit parametrii în parametrul de tip generic. Trebuie să legăm
Par
la"userID"
, chiar dacă l-am specifica oricum în argumentul cale al funcției. Acesta nu este JavaScript-y! - Această abordare gestionează doar un parametru de rută. În momentul în care adăugăm o uniune, de exemplu
"userID" | "orderId"
"userID" | "orderId"
verificarea de siguranță este satisfăcută cu doar unul dintre aceste argumente fiind disponibil. Așa funcționează seturile. Poate fi unul, sau altul.
Trebuie să existe o cale mai bună. Si aici este. Altfel, acest articol s-ar termina cu o notă foarte amară.
Să inversăm ordinea! Să nu încercăm să definim parametrii rutei într-o variabilă de tip generic, ci mai degrabă să extragem variabilele din path
pe care o trecem ca prim argument al app.get
.
Pentru a ajunge la valoarea reală, trebuie să vedem cum funcționează legarea generică în TypeScript. Să luăm de exemplu această funcție de identity
:
function identity<T>(inp: T) : T { return inp }
Poate fi cea mai plictisitoare funcție generică pe care ați văzut-o vreodată, dar ilustrează perfect un punct. identity
ia un argument și returnează din nou aceeași intrare. Tipul este tipul generic T
și returnează, de asemenea, același tip.
Acum putem lega T
de string
, de exemplu:
const z = identity<string>("yes"); // z is of type string
Această legare generică în mod explicit se asigură că transmitem numai strings
de caractere către identity
și, deoarece legăm explicit, tipul returnat este, de asemenea, string
. Dacă uităm să legăm, se întâmplă ceva interesant:
const y = identity("yes") // y is of type "yes"
În acest caz, TypeScript deduce tipul din argumentul pe care îl transmiteți și leagă T
de tipul literal șir "yes"
. Aceasta este o modalitate excelentă de a converti un argument de funcție într-un tip literal, pe care apoi îl folosim în celelalte tipuri generice ale noastre.
Să facem asta adaptând app.get
.
function get<Path extends string = string>( path: Path, callback: CallbackFn<"GET", ParseRouteParams<Path>> ) { // to be implemented }
Îndepărtăm tipul generic Par
și adăugăm Path
. Path
poate fi un subset al oricărui string
. Setăm path
acestui tip generic Path
, ceea ce înseamnă că în momentul în care trecem un parametru pentru a get
, prindem tipul său literal șir. Trecem Path
către un nou tip generic ParseRouteParams
pe care nu l-am creat încă.
Să lucrăm la ParseRouteParams
. Aici, schimbăm din nou ordinea evenimentelor. În loc să trecem parametrii de rută solicitați la generic pentru a ne asigura că calea este în regulă, trecem calea rutei și extragem parametrii de rută posibili. Pentru aceasta, trebuie să creăm un tip condiționat.
Tipuri condiționale și tipuri de șabloane recursive literale
Tipurile condiționate sunt similare sintactic cu operatorul ternar din JavaScript. Verificați o condiție, iar dacă condiția este îndeplinită, returnați filiala A, în caz contrar, returnați filiala B. De exemplu:
type ParseRouteParams<Rte> = Rte extends `${string}/:${infer P}` ? P : never;
Aici, verificăm dacă Rte
este un subset al fiecărei căi care se termină cu parametrul la sfârșitul stil Express (cu un "/:"
precedent). Dacă da, deducem acest șir. Ceea ce înseamnă că îi captăm conținutul într-o nouă variabilă. Dacă condiția este îndeplinită, returnăm șirul nou extras, în caz contrar, nu revenim niciodată, ca în: „Nu există parametri de rută”,
Dacă îl încercăm, obținem ceva de genul:
type Params = ParseRouteParams<"/api/user/:userID"> // Params is "userID" type NoParams = ParseRouteParams<"/api/user"> // NoParams is never --> no params!
Grozav, asta e deja mult mai bine decât am făcut-o mai devreme. Acum, vrem să prindem toți ceilalți parametri posibili. Pentru asta, trebuie să adăugăm o altă condiție:
type ParseRouteParams<Rte> = Rte extends `${string}/:${infer P}/${infer Rest}` ? P | ParseRouteParams<`/${Rest}`> : Rte extends `${string}/:${infer P}` ? P : never;
Tipul nostru condiționat funcționează acum după cum urmează:
- În prima condiție, verificăm dacă există un parametru de rută undeva între rută. Dacă da, extragem atât parametrul rutei, cât și tot ce urmează după aceea. Returnăm parametrul rută
P
nou găsit într-o uniune în care apelăm recursiv același tip generic cuRest
. De exemplu, dacă trecem ruta"/api/users/:userID/orders/:orderID"
laParseRouteParams
, deducem"userID"
înP
și"orders/:orderID"
înRest
. Numim același tip cuRest
- Aici intervine a doua condiție. Aici verificăm dacă există un tip la sfârșit. Acesta este cazul pentru
"orders/:orderID"
. Extragem"orderID"
și returnăm acest tip literal. - Dacă nu mai rămâne niciun parametru de rută, nu ne întoarcem niciodată.
Dan Vanderkam arată un tip similar și mai elaborat pentru ParseRouteParams
, dar și cel pe care îl vedeți mai sus ar trebui să funcționeze. Dacă încercăm ParseRouteParams
nou adaptat, obținem ceva de genul acesta:
// Params is "userID" type Params = ParseRouteParams<"/api/user/:userID"> // MoreParams is "userID" | "orderID" type MoreParams = ParseRouteParams<"/api/user/:userID/orders/:orderId">
Să aplicăm acest nou tip și să vedem cum arată utilizarea noastră finală a app.get
.
app.get("/api/users/:userID/orders/:orderID", function (req, res) { req.params.userID; // YES!! req.params.orderID; // Also YES!!! });
Wow. Acesta arată ca codul JavaScript pe care l-am avut la început!
Tipuri statice pentru comportament dinamic
Tipurile pe care tocmai le-am creat pentru o funcție app.get
se asigură că excludem o mulțime de erori posibile:
- Putem transmite doar codurile numerice adecvate de stare către
res.status()
-
req.method
este unul dintre cele patru șiruri posibile, iar când folosimapp.get
, știm că este doar"GET"
- Putem analiza parametrii rutei și să ne asigurăm că nu avem greșeli de scriere în apelul înapoi
Dacă ne uităm la exemplul de la începutul acestui articol, primim următoarele mesaje de eroare:
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'? }); } })
Și toate acestea înainte de a rula codul nostru! Serverele în stil expres sunt un exemplu perfect al naturii dinamice a JavaScript. În funcție de metoda pe care o apelați, șirul pe care îl transmiteți pentru primul argument, multe schimbări de comportament în interiorul callback-ului. Luați un alt exemplu și toate tipurile dvs. arată complet diferit.
Dar cu câteva tipuri bine definite, putem surprinde acest comportament dinamic în timp ce ne edităm codul. În timpul compilării cu tipuri statice, nu în timpul execuției, când lucrurile merg în plină expansiune!
Și aceasta este puterea TypeScript. Un sistem de tip static care încearcă să oficializeze tot comportamentul dinamic JavaScript pe care îl cunoaștem cu toții atât de bine. Dacă doriți să încercați exemplul pe care tocmai l-am creat, mergeți la locul de joacă TypeScript și jucați-vă cu el.
În acest articol, am atins multe concepte. Dacă doriți să aflați mai multe, consultați TypeScript în 50 de lecții, unde veți primi o introducere blândă în sistemul de tipărire în lecții mici, ușor de digerat. Versiunile de cărți electronice sunt disponibile imediat, iar cartea tipărită va fi o referință excelentă pentru biblioteca dvs. de codare.