TypeScript avansat cu un exemplu real (Partea 1)
Publicat: 2020-11-06Săptămâna trecută am văzut o mică introducere în TypeScript și, în special, am vorbit despre modul în care acest limbaj care extinde JavaScript ne poate ajuta să creăm un cod mai robust. Deoarece aceasta a fost doar o introducere, nu am vorbit despre unele dintre caracteristicile TypeScript pe care ați putea dori (și probabil să aveți nevoie) să le utilizați în proiectele dvs.
Astăzi vă voi învăța cum să aplicați TypeScript profesional într-un proiect real. Pentru a face acest lucru, vom începe prin a analiza o parte din codul sursă al Nelio Content pentru a înțelege de unde începem și ce limitări avem în prezent. În continuare, vom îmbunătăți treptat codul JavaScript original adăugând mici îmbunătățiri incrementale până când vom avea un cod complet tastat.
Folosind codul sursă al Nelio Content ca bază
După cum probabil știți deja, Nelio Content este un plugin care vă permite să partajați conținutul site-ului dvs. pe rețelele sociale. În plus, include și câteva funcționalități care vizează să vă ajute să generați constant conținut mai bun pe blogul dvs., cum ar fi o analiză de calitate a postărilor dvs., un calendar editorial pentru a urmări conținutul viitor pe care trebuie să îl scrieți și așa mai departe .

Luna trecută am publicat versiunea 2.0, o reproiectare completă atât vizual, cât și intern a pluginului nostru. Am creat această versiune folosind toate noile tehnologii pe care le avem la dispoziție astăzi în WordPress (ceva despre care am vorbit recent pe blogul nostru), inclusiv o interfață React și un magazin Redux.
Deci, în exemplul de astăzi îl vom îmbunătăți pe acesta din urmă. Adică, vom vedea cum putem introduce un magazin Redux.
Nelio Content Editor Calendar Selecters
Calendarul editorial este o interfață de utilizator care arată postările de blog pe care le-am programat pentru fiecare zi a săptămânii. Aceasta înseamnă că, cel puțin, magazinul nostru Redux va avea nevoie de două operațiuni de interogare: una care ne spune postările care sunt programate într-o anumită zi și alta care, având în vedere un ID de postare, returnează toate atributele sale.
Presupunând că ați citit postările noastre pe acest subiect, știți deja că un selector în Redux primește ca prim parametru starea cu toate informațiile aplicației noastre urmate de orice parametri suplimentari de care ar putea avea nevoie. Deci, cele două exemple de selectoare în JavaScript ar fi cam așa:
function getPost( state, id ) { return state.posts[ id ]; } function getPostsInDay( state, day ) { return state.days[ day ] ?? []; } Dacă vă întrebați de unde știu că un stat are atributele posts și days , este destul de simplu: pentru că eu sunt cel care le-am definit. Dar iată de ce am decis să le implementez astfel.
Știm că dorim să ne putem accesa informațiile din două puncte de vedere diferite: postări într-o zi sau postări prin ID. Deci, se pare că are sens să ne organizăm datele în două părți:
- Pe de o parte, avem un atribut
postsîn care am enumerat toate postările pe care le-am obținut de pe server și le-am salvat în magazinul nostru Redux. În mod logic, am fi putut să le salvăm într-o matrice și să facem o căutare secvențială pentru a găsi postarea al cărei ID se potrivește cu cel așteptat... dar un obiect se comportă ca un dicționar, oferind căutări mai rapide. - Pe de altă parte, trebuie să accesăm și postările care sunt programate într-o anumită zi. Din nou, am fi putut folosi doar o singură matrice pentru a stoca toate postările și a le filtra pentru a găsi postările care aparțin unei anumite zile, dar un alt dicționar oferă o soluție de căutare mai rapidă.
Acțiuni și reduceri în conținutul Nelio
În sfârșit, dacă dorim un calendar dinamic, trebuie să implementăm funcții care să ne permită să actualizăm informațiile pe care le stochează magazinul nostru. Pentru simplitate, vom propune două metode simple: una care ne permite să adăugăm noi postări în calendar și alta care ne permite să modificăm atributele celor existente.
Actualizările unui magazin Redux necesită două părți. Pe de o parte, avem acțiuni care semnalează schimbarea pe care dorim să o facem și, pe de altă parte, există un reductor care, având în vedere starea actuală a magazinului nostru și o acțiune prin care se solicită o actualizare, aplică modificările necesare în starea actuală. generează o nouă stare.
Deci, ținând cont de acest lucru, acestea sunt acțiunile pe care le-am putea avea în magazinul nostru:
function receiveNewPost( post ) { return { type: 'RECEIVE_NEW_POST', post, }; } function updatePost( postId, attributes ) { return { type: 'UPDATE_POST', postId, attributes, } }si iata reductorul:
function reducer( state, action ) { state = state ?? { posts: {}, days: {} }; const postIds = Object.keys( state.posts ); switch ( action.type ) { case 'RECEIVE_NEW_POST'; if ( postIds.includes( action.postId ) ) { return state; } return { posts: { ...state.posts, [ action.post.id ]: action.post, }, days: { ...state.days, [ action.post.day ]: [ ...state.days[ action.post.day ], action.post.id, ], }, }; case 'UPDATE_POST'; if ( ! postIds.includes( action.postId ) ) { return state; } const post = { ...state.posts[ action.postId ], ...action.attributes, }; return { posts: { ...state.posts, [ post.id ]: post, }, days: { ...Object.keys( state.days ).reduce( ( acc, day ) => ( { ...acc, [ day ]: state.days[ day ].filter( ( postId ) => postId !== post.id ), } ), {} ), [ post.day ]: [ ...state.days[ post.day ], post.id, ], }, }; } return state; }Fă-ți timp să înțelegi totul și să mergem înainte!
De la JavaScript la TypeScript
Primul lucru pe care ar trebui să-l facem este să traducem codul anterior în TypeScript. Ei bine, din moment ce TypeScript este un superset de JavaScript, este deja... dar dacă copiați și lipiți funcțiile anterioare în TypeScript Playground, veți vedea că compilatorul se plânge destul de mult pentru că există prea multe variabile al căror tip implicit este any . Așa că, mai întâi, să reparăm asta adăugând în mod explicit câteva tipuri de bază.
Tot ce trebuie să facem este să adăugăm în mod explicit any tip la orice este „complex” (cum ar fi starea aplicației noastre) și să folosim number sau string sau orice vrem la orice altă variabilă/argument. De exemplu, selectorul JavaScript original:
function getPost( state, id ) { return state.posts[ id ]; }cu tipuri explicite TypeScript ar arăta astfel:
function getPost( state: any, id: number ): any | undefined { return state.posts[ id ]; } După cum puteți vedea, simpla acțiune de tastare a codului nostru (chiar și atunci când folosim „tipuri generice”) oferă o mulțime de informații cu o privire rapidă; o îmbunătățire clară în comparație cu JavaScript de bază! În acest caz, de exemplu, vedem că getPost așteaptă un number (un ID de postare este un număr întreg, vă amintiți?) și rezultatul va fi fie ceva dacă postarea există ( any ) sau nimic dacă nu există ( undefined ).
Aici aveți link-ul cu toate tipurile de cod folosind tipuri simple, astfel încât compilatorul să nu se plângă.
Creați și utilizați tipuri de date personalizate în TypeScript
Acum că compilatorul este mulțumit de codul nostru sursă, este timpul să ne gândim puțin la cum îl putem îmbunătăți. Pentru aceasta, îmi propun întotdeauna să începem prin a modela conceptele pe care le avem în domeniul nostru.

Crearea unui tip personalizat pentru postări
Știm că magazinul nostru va conține în primul rând postări, așa că aș spune că primul pas este să modelăm ce este o postare și ce informații avem despre ea. Am văzut deja cum să creăm tipuri personalizate săptămâna trecută, așa că haideți să încercăm astăzi cu conceptul postării:
type Post = { id: number; title: string; author: string; day: string; status: string; isSticky: boolean; }; Nicio surpriză aici, nu? O Post este un obiect care are câteva atribute, cum ar fi un id numeric , un title de text și așa mai departe.
O altă informație importantă pe care o are orice magazin Redux este, ați ghicit, starea acestuia. În secțiunea anterioară am discutat deja despre atributele pe care le are, așa că haideți să definim forma de bază a tipului nostru de State :
type State = { posts: any; days: any; }; Îmbunătățirea tipului de State
Acum știm că State are două atribute ( posts și days ), dar nu știm prea multe despre ele, deoarece pot fi any . Am spus că am vrut ca ambele atribute să fie dicționare. Adică, având în vedere o anumită interogare (fie un ID de postare pentru posts fie o dată pentru days ), dorim datele aferente (o postare sau, respectiv, o listă de postări). Știm că putem implementa un dicționar folosind un obiect, dar cum reprezentăm un dicționar în TypeScript?
Dacă aruncăm o privire la documentația TypeScript, vom vedea că aceasta include mai multe tipuri de utilitare pentru a face față situațiilor destul de comune. Mai exact, există un tip numit Record care pare să fie cel pe care îl dorim: ne permite să introducem o variabilă folosind perechi cheie/valoare în care cheia are un anumit tip Keys și valorile sunt de tip Type . Dacă aplicăm acest tip exemplului nostru, am ajunge la ceva de genul acesta:
type State = { posts: Record<number, Post>; days: Record<string, number[]>; }; Din perspectiva compilatorului, tipul Record funcționează în așa fel încât, având în vedere orice valoare a Keys (în exemplul nostru, number pentru posts și string pentru days ), rezultatul său va fi întotdeauna un obiect de tip Type (în cazul nostru, un Post sau, respectiv, un number[] ). Problema este că nu așa vrem să se comporte dicționarul nostru: atunci când căutăm o anumită postare folosind ID-ul său, dorim ca compilatorul să știe că este posibil să găsim sau nu o postare înrudită, ceea ce înseamnă că rezultatul poate fi fie un Post sau undefined .
Din fericire, putem rezolva cu ușurință acest lucru utilizând încă un alt tip de utilitate, tipul Partial :
type State = { posts: Partial< Record<number, Post> >; days: Partial< Record<string, number[]> >; };Îmbunătățirea codului nostru cu aliasuri de tip
Aruncă o privire la atributul posts din statul nostru... Ce vezi? Un dicționar care indexează postările de tip Post cu numere, nu? Acum imaginați-vă că revizuiți acest cod la locul de muncă. Dacă întâlniți un astfel de tip, ați putea presupune că un number de postări de indexare este probabil ID-ul postărilor indexate... dar aceasta este doar o presupunere; ar trebui să revizuiți codul pentru a fi sigur de el. Și ce zici de days ? „Șiruri aleatorii care indexează liste de numere.” Nu este de mare ajutor, nu-i așa?
Tipurile TypeScript ne ajută să scriem cod mai robust datorită verificărilor compilatorului, dar oferă mult mai mult decât atât. Dacă utilizați tipuri semnificative, codul dvs. va fi mai bine documentat și va fi mai ușor de întreținut. Deci, haideți să aliam tipurile existente pentru a crea tipuri semnificative, nu?
De exemplu, știind că ID-urile postării ( number ) și datele ( string ) sunt relevante pentru domeniul nostru, putem crea cu ușurință următoarele aliasuri de tip:
type PostId = number; type Day = string;și apoi rescrieți tipurile noastre originale folosind aceste alias-uri:
type Post = { id: PostId; title: string; author: string; day: Day; status: string; isSticky: boolean; }; type State = { posts: Partial< Record<PostId, Post> >; days: Partial< Record<Day, PostId[]> >; }; Un alt tip de alias pe care îl putem folosi pentru a îmbunătăți lizibilitatea codului nostru este tipul Dictionary , care „ascunde” complexitatea utilizării Partial și Record în spatele unei structuri convenabile:
type Dictionary<K extends string | number, T> = Partial< Record<K, T> >;facem codul sursă mai clar:
type State = { posts: Dictionary<PostId, Post>; days: Dictionary<Day, PostId[]>; }; Si asta e! Iată-l! Cu doar trei aliasuri simple, am putut documenta codul într-un mod clar mai bun decât folosind comentarii. Orice dezvoltator care vine după noi va putea ști, dintr-o privire, că posts este un dicționar care indexează obiecte de tip Post folosind PostId -ul lor și că days este o structură de date care, dată fiind o Day , returnează identificatorii de postare în listă. E destul de grozav, dacă mă întrebi pe mine.
Dar nu numai definițiile de tip în sine sunt mai bune... dacă folosim aceste tipuri noi în tot codul nostru:
function getPost( state: State, id: PostId ): Post | undefined { return state.posts[ id ]; }beneficiază și de acest nou strat semantic! Puteți vedea noua versiune a codului nostru introdus aici.
Apropo, rețineți că aliasurile de tip sunt, din punctul de vedere al compilatorului, imposibil de distins de tipul „original”. Aceasta înseamnă că, de exemplu, un PostId și un number sunt complet interschimbabile. Deci, nu vă așteptați ca compilatorul să declanșeze o eroare dacă atribuiți un PostId unui number sau invers (după cum puteți vedea în acest mic exemplu); ele servesc pur și simplu pentru a adăuga semantică la codul nostru sursă.
Pasii urmatori
După cum puteți vedea, puteți introduce cod JavaScript folosind tipuri TypeScript în mod incremental și, făcând acest lucru, calitatea și lizibilitatea acestuia se îmbunătățesc. În postarea de astăzi am văzut în detaliu un exemplu de implementare reală a unei aplicații React + Redux și am văzut cum ar putea fi îmbunătățită cu relativ puțin efort. Dar avem încă un drum lung de parcurs.
În următoarea postare vom introduce toate variabilele/argumentele rămase care folosesc în prezent any tip și vom învăța, de asemenea, câteva fapte avansate TypeScript. Sper că ți-a plăcut această primă parte și, dacă ți-a plăcut, te rog să o împărtășești prietenilor și colegilor tăi.
Imagine prezentată de Danielle MacInnes pe Unsplash.
