TypeScript avanzato con un esempio reale (parte 1)
Pubblicato: 2020-11-06La scorsa settimana abbiamo visto una piccola introduzione a TypeScript e, in particolare, abbiamo parlato di come questo linguaggio che estende JavaScript può aiutarci a creare codice più robusto. Dato che era solo un'introduzione, non ho parlato di alcune delle funzionalità di TypeScript che potresti voler (e probabilmente dover) utilizzare nei tuoi progetti.
Oggi ti insegnerò come applicare TypeScript professionalmente in un progetto reale. Per fare ciò, inizieremo esaminando una parte del codice sorgente di Nelio Content per capire da dove iniziamo e quali limitazioni abbiamo attualmente. Successivamente, miglioreremo gradualmente il codice JavaScript originale aggiungendo piccoli miglioramenti incrementali fino a quando non avremo un codice completamente tipizzato.
Utilizzando il codice sorgente di Nelio Content come base
Come forse già saprai, Nelio Content è un plugin che ti permette di condividere il contenuto del tuo sito web sui social media. Oltre a questo, include anche diverse funzionalità che mirano ad aiutarti a generare costantemente contenuti migliori sul tuo blog, come un'analisi della qualità dei tuoi post, un calendario editoriale per tenere traccia dei contenuti imminenti che devi scrivere e così via .

Il mese scorso abbiamo pubblicato la versione 2.0, una riprogettazione completa sia visivamente che internamente del nostro plugin. Abbiamo creato questa versione utilizzando tutte le nuove tecnologie che abbiamo oggi a disposizione in WordPress (qualcosa di cui abbiamo parlato di recente nel nostro blog), inclusa un'interfaccia React e un negozio Redux.
Quindi, nell'esempio di oggi miglioreremo quest'ultimo. Cioè, vedremo come possiamo digitare un negozio Redux.
Selettori del calendario editoriale di Nelio Content
Il calendario editoriale è un'interfaccia utente che mostra i post del blog che abbiamo programmato per ogni giorno della settimana. Ciò significa che, come minimo, il nostro negozio Redux avrà bisogno di due operazioni di query: una che ci dice i post che sono programmati in un dato giorno e un'altra che, dato un ID post, restituisce tutti i suoi attributi.
Supponendo che tu abbia letto i nostri post sull'argomento, sai già che un selettore in Redux riceve come primo parametro lo stato con tutte le informazioni della nostra app seguite da eventuali parametri aggiuntivi di cui potrebbe aver bisogno. Quindi i nostri due selettori di esempio in JavaScript sarebbero qualcosa del genere:
function getPost( state, id ) { return state.posts[ id ]; } function getPostsInDay( state, day ) { return state.days[ day ] ?? []; } Se ti stai chiedendo come faccio a sapere che uno stato ha gli attributi posts e days , è abbastanza semplice: perché sono io che li ho definiti. Ma ecco perché ho deciso di implementarli in questo modo.
Sappiamo che vogliamo poter accedere alle nostre informazioni da due diversi punti di vista: post in un giorno o post per ID. Quindi sembra che abbia senso organizzare i nostri dati in due parti:
- Da un lato, abbiamo un attributo
postsin cui abbiamo elencato tutti i post che abbiamo ottenuto dal server e salvati nel nostro negozio Redux. Logicamente, avremmo potuto salvarli in un array e fare una ricerca sequenziale per trovare il post il cui ID corrisponde a quello previsto… ma un oggetto si comporta come un dizionario, offrendo ricerche più veloci. - D'altra parte, dobbiamo anche accedere ai post che sono programmati in un determinato giorno. Ancora una volta, avremmo potuto utilizzare un solo array per archiviare tutti i post e filtrarlo per trovare i post che appartengono a un determinato giorno, ma avere un altro dizionario offre una soluzione di ricerca più rapida.
Azioni e riduttori in Nelio Content
Infine, se vogliamo un calendario dinamico, dobbiamo implementare funzioni che ci consentano di aggiornare le informazioni che il nostro negozio memorizza. Per semplicità, proporremo due semplici metodi: uno che ci consente di aggiungere nuovi post al calendario e un altro che ci consente di modificare gli attributi di quelli esistenti.
Gli aggiornamenti a un negozio Redux richiedono due parti. Da un lato abbiamo azioni che segnalano il cambiamento che vogliamo apportare e, dall'altro, un riduttore che, dato lo stato attuale del nostro negozio e un'azione che richiede un aggiornamento, applica le modifiche necessarie allo stato attuale per generare un nuovo stato.
Quindi, tenendo conto di questo, queste sono le azioni che potremmo avere nel nostro negozio:
function receiveNewPost( post ) { return { type: 'RECEIVE_NEW_POST', post, }; } function updatePost( postId, attributes ) { return { type: 'UPDATE_POST', postId, attributes, } }ed ecco il riduttore:
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; }Prenditi il tuo tempo per capire tutto e andiamo avanti!
Da JavaScript a TypeScript
La prima cosa che dovremmo fare è tradurre il codice precedente in TypeScript. Bene, poiché TypeScript è un superset di JavaScript, lo è già... ma se copi e incolli le funzioni precedenti nel TypeScript Playground, vedrai che il compilatore si lamenta un po' perché ci sono troppe variabili il cui tipo implicito è any . Quindi risolviamo prima il problema aggiungendo esplicitamente alcuni tipi di base.
Tutto quello che dobbiamo fare è aggiungere esplicitamente il tipo any a tutto ciò che è "complesso" (come lo stato della nostra applicazione) e utilizzare number o string o qualsiasi altra cosa desideriamo per qualsiasi altra variabile/argomento. Ad esempio, il selettore JavaScript originale:
function getPost( state, id ) { return state.posts[ id ]; }con tipi TypeScript espliciti sarebbe simile a questo:
function getPost( state: any, id: number ): any | undefined { return state.posts[ id ]; } Come puoi vedere, la semplice azione di digitare il nostro codice (anche quando utilizziamo "tipi generici") offre molte informazioni con una rapida occhiata; un netto miglioramento rispetto al JavaScript di base! In questo caso, ad esempio, vediamo che getPost si aspetta un number (un ID post è un numero intero, ricordi?) e il risultato sarà qualcosa se il post esiste ( any ) o niente se non lo è ( undefined ).
Qui hai il collegamento con tutto il tipo di codice usando i tipi semplici in modo che il compilatore non si lamenti.
Crea e usa tipi di dati personalizzati in TypeScript
Ora che il compilatore è soddisfatto del nostro codice sorgente, è tempo di pensare un po' a come possiamo migliorarlo. Per questo, propongo sempre di iniziare modellando i concetti che abbiamo nel nostro dominio.

Creazione di un tipo personalizzato per i post
Sappiamo che il nostro negozio conterrà principalmente post, quindi direi che il primo passo è modellare cos'è un post e quali informazioni abbiamo a riguardo. Abbiamo già visto come creare tipi personalizzati la scorsa settimana, quindi proviamoci oggi con il concetto di post:
type Post = { id: number; title: string; author: string; day: string; status: string; isSticky: boolean; }; Nessuna sorpresa qui, giusto? Un Post è un oggetto che ha alcuni attributi, come un id numerico , un title di testo e così via.
Un'altra informazione importante che ha qualsiasi negozio Redux è, hai indovinato, il suo stato. Nella sezione precedente abbiamo già discusso gli attributi che ha, quindi definiamo la forma base del nostro tipo di State :
type State = { posts: any; days: any; }; Miglioramento del tipo di State
Ora sappiamo che lo State ha due attributi ( posts e days ), ma non sappiamo molto di cosa siano, poiché possono essere any cosa. Abbiamo detto che volevamo che entrambi gli attributi fossero dizionari. Cioè, data una determinata query (un ID post per posts o una data per days ), vogliamo i dati correlati (rispettivamente un post o un elenco di post). Sappiamo che possiamo implementare un dizionario usando un oggetto, ma come rappresentiamo un dizionario in TypeScript?
Se diamo un'occhiata alla documentazione di TypeScript, vedremo che include diversi tipi di utilità per affrontare situazioni abbastanza comuni. Nello specifico, esiste un tipo chiamato Record che sembra essere quello che vogliamo: ci permette di digitare una variabile utilizzando coppie chiave/valore in cui la chiave ha un certo tipo di Keys e i valori sono di tipo Type . Se applichiamo questo tipo al nostro esempio, ci ritroveremo con qualcosa del genere:
type State = { posts: Record<number, Post>; days: Record<string, number[]>; }; Dal punto di vista del compilatore, il tipo Record funziona in modo tale che, dato qualsiasi valore di Keys (nel nostro esempio, number per posts e string per days ), il suo risultato sarà sempre un oggetto di tipo Type (nel nostro caso, un Post o un number[] , rispettivamente). Il problema è che non è così che vogliamo che si comporti il nostro dizionario: quando cerchiamo un post specifico usando il suo ID, vogliamo che il compilatore sappia che potremmo o meno trovare un post correlato, il che significa che il risultato può essere un Post o undefined .
Fortunatamente, possiamo risolvere facilmente questo problema utilizzando ancora un altro tipo di utilità, il tipo Partial :
type State = { posts: Partial< Record<number, Post> >; days: Partial< Record<string, number[]> >; };Migliorare il nostro codice con alias di tipo
Dai un'occhiata all'attributo dei posts nel nostro stato... Cosa vedi? Un dizionario che indicizza i Post di tipo Post con numeri, giusto? Ora immaginati mentre rivedi questo codice al lavoro. Se incontri un tipo del genere, potresti presumere che un number di post di indicizzazione sia probabilmente l'ID dei post indicizzati ... ma questa è solo un'ipotesi; dovresti rivedere il codice per esserne sicuro. E che dire dei days ? "Stringhe casuali che indicizzano elenchi di numeri." Non è molto utile, vero?
I tipi TypeScript ci aiutano a scrivere codice più robusto grazie ai controlli del compilatore, ma offrono molto di più. Se utilizzi tipi significativi, il tuo codice sarà meglio documentato e sarà più facile da mantenere. Quindi definiamo i tipi esistenti per creare tipi significativi, vero?
Ad esempio, sapendo che gli ID post ( number ) e le date ( string ) sono rilevanti per il nostro dominio, possiamo facilmente creare i seguenti alias di tipo:
type PostId = number; type Day = string;e quindi riscrivi i nostri tipi originali usando questi alias:
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 altro alias di tipo che possiamo utilizzare per migliorare la leggibilità del nostro codice è il tipo Dictionary , che "nasconde" la complessità dell'utilizzo di Partial e Record dietro una struttura conveniente:
type Dictionary<K extends string | number, T> = Partial< Record<K, T> >;rendendo il nostro codice sorgente più chiaro:
type State = { posts: Dictionary<PostId, Post>; days: Dictionary<Day, PostId[]>; }; E questo è tutto! Ecco qua! Con solo tre semplici alias di tipo siamo stati in grado di documentare il codice in un modo chiaramente migliore rispetto all'utilizzo dei commenti. Qualsiasi sviluppatore che verrà dopo di noi sarà in grado di sapere, a colpo d'occhio, che posts è un dizionario che indicizza oggetti di tipo Post utilizzando il loro PostId e che days è una struttura dati che, dato un Day , restituisce un elenco di identificatori di post. È davvero fantastico, se me lo chiedi.
Ma non solo le definizioni di tipo stesse sono migliori... se utilizziamo questi nuovi tipi in tutto il nostro codice:
function getPost( state: State, id: PostId ): Post | undefined { return state.posts[ id ]; }beneficia anche di questo nuovo livello semantico! Puoi vedere la nuova versione del nostro codice digitato qui.
Oh, a proposito, tieni presente che gli alias di tipo sono, dal punto di vista del compilatore, indistinguibili dal tipo "originale". Ciò significa che, ad esempio, un PostId e un number sono completamente intercambiabili. Quindi non aspettarti che il compilatore generi un errore se assegni un PostId a un number o viceversa (come puoi vedere in questo piccolo esempio); servono semplicemente ad aggiungere semantica al nostro codice sorgente.
Prossimi passi
Come puoi vedere, puoi digitare codice JavaScript utilizzando i tipi TypeScript in modo incrementale e, così facendo, la sua qualità e leggibilità migliorano. Nel post di oggi abbiamo visto in dettaglio un esempio di implementazione reale di un'applicazione React + Redux e abbiamo visto come potrebbe essere migliorata con relativamente poco sforzo. Ma abbiamo ancora molta strada da fare.
Nel prossimo post digiteremo tutte le restanti variabili/argomenti che stanno attualmente utilizzando il tipo any e impareremo anche alcune funzionalità avanzate di TypeScript. Spero che questa prima parte vi sia piaciuta e, se l'avete fatto, condividetela con i vostri amici e colleghi.
Immagine in primo piano di Danielle MacInnes su Unsplash.
