Nel corso del lavoro su un progetto web in React<\/a>, in un qualsiasi framework che lo utilizzi, o anche in altri progetti JavaScript, \u00e8 possibile ritrovarsi nella situazione in cui bisogna recuperare dati da fonti esterne.<\/p>\n\n\n\n C\u2019\u00e8 una grande quantit\u00e0 e variet\u00e0 di fonti dati: API web, SDK, documenti sul file system, il localStorage<\/strong><\/a> del browser, una query string. Spesso, ad esempio, pu\u00f2 capitare di dover reperire dati serializzati in forma testuale che magari noi stessi avevamo salvato da qualche parte per memorizzare una scelta dell\u2019utente.<\/p>\n\n\n\n Leggere queste informazioni, che lo si faccia manualmente con gli strumenti nativi di JavaScript o che si usi una libreria, tipicamente \u00e8 piuttosto semplice: si interroga la fonte dati, si ottiene un elemento o una lista ed \u00e8 tutto pronto. Tipicamente ci\u00f2 avviene con campi numerici, ma pu\u00f2 riguardare qualunque altro tipo di dato non testuale: abbiamo un valore numerico salvato da qualche parte, lo recuperiamo da una fonte dati e lo usiamo in un confronto o in qualche altra operazione aspettandoci che sia un number<\/strong>, per poi scoprire a runtime che in realt\u00e0 si tratta di una stringa, ritrovandoci con bug subdoli e poco evidenti a prima vista. Vediamo un esempio pratico.<\/p>\n\n\n\n Immaginiamo una situazione che sar\u00e0 sicuramente capitata a chiunque si trovi nello sviluppo web: stiamo creando un\u2019applicazione React che salva in query string i dati di una deliziosa pizza<\/strong>, e in seguito li recupera per mostrarli all\u2019utente.<\/p>\n\n\n\n Creiamo allora un nuovo progetto create-react-app<\/a>, con TypeScript come piace a noi, e mettiamoci al lavoro.<\/p>\n\n\n\n Per serializzare e deserializzare gli oggetti useremo la libreria Qs<\/strong><\/a>, con react-router<\/strong><\/a> e react-router-dom<\/strong><\/a> per la manipolazione della query string, senza dimenticare le dichiarazioni TypeScript.<\/p>\n\n\n\n Innanzitutto, creiamo un semplice modello dati per la nostra pizza, con un ID e un paio di campi testuali.<\/p>\n\n\n\n La nostra applicazione web avr\u00e0 due componenti.<\/p>\n\n\n\n I componenti si troveranno all\u2019interno del contenitore PizzaWrapper<\/strong>\u2026<\/p>\n\n\n\n \u2026che sar\u00e0 la root della navigazione.<\/p>\n\n\n\n PizzaWriter<\/strong> \u00e8 semplice: alla pressione di un pulsante, serializza un oggetto pizza con Qs<\/strong> e salva il risultato in query string con l\u2019hook di react-router-dom<\/strong> useSearchParams<\/strong><\/a>.<\/p>\n\n\n\n PizzaReader<\/strong> \u00e8 dove le cose iniziano a complicarsi un po\u2019. Di base, quel che vogliamo \u00e8 stare in ascolto sulla query string, sempre con useSearchParams<\/strong>, per essere pronti a ricevere una Pizza<\/strong> e metterla nello stato. Appena arriva, mostriamo all\u2019utente i dati della Pizza<\/strong>. Ci aspettiamo di ricevere una margherita, quindi controlliamo anche, in base all\u2019ID, che la pizza sia quella che abbiamo ordinato.<\/p>\n\n\n\n Ma come facciamo a essere sicuri che l\u2019oggetto che leggiamo sia proprio una Pizza<\/strong>?<\/p>\n\n\n\n Una strada potrebbe essere quella di definire una type guard<\/a>, per assicurarci che l\u2019oggetto in query string abbia i campi che ci aspettiamo.<\/p>\n\n\n\n Proviamo a completare la useEffect<\/strong> di PizzaReader<\/strong> cos\u00ec.<\/p>\n\n\n\n Dal punto di vista di TypeScript, \u00e8 tutto a posto. Facciamo partire la nostra applicazione con\u2026<\/p>\n\n\n\n \u2026e apriamo il browser, per vedere PizzaReader<\/strong> pronto a ricevere una Pizza<\/strong>. Premiamo senza indugio il pulsante per consegnare il nostro stupendo pacchetto di amore e carboidrati, e vediamo come cambia la situazione.<\/p>\n\n\n\n A una prima occhiata \u00e8 tutto a posto, ma qualcosa non va. I dati della Pizza<\/strong> sembrano corretti, se non per il fatto che PizzaReader<\/strong> non vede la Pizza<\/strong> come una margherita. Come mai? La chiave \u00e8 nel confronto che stiamo facendo sull\u2019ID.<\/p>\n\n\n\n Il problema \u00e8 che la pizzaToRead<\/strong> \u00e8 stata definita nel nostro codice, rispettando l\u2019interfaccia definita, mentre pizza<\/strong> viene recuperata dalla query string. Nel primo caso, l\u2019ID viene correttamente impostato come un number<\/strong>; nel secondo, per\u00f2, non avendo la query string nessuna indicazione sul tipo delle variabili, tutti i valori avranno tipo string<\/strong> a runtime. La strict equality<\/a> che ci aspettiamo, dunque, non \u00e8 rispettata: i tipi sono diversi, anche se TypeScript non pu\u00f2 saperlo.<\/p>\n\n\n\n Risolvere questa situazione non \u00e8 banale come pu\u00f2 sembrare. Certo, per un caso cos\u00ec semplice potremmo usare la semplice equality<\/a>, ma in situazioni pi\u00f9 complesse? Se dovessimo usare un metodo specifico di String<\/strong> o di Number<\/strong>?<\/p>\n\n\n\n Si potrebbe pensare di rendere pi\u00f9 stretta la type guard isPizza<\/strong> per controllare anche il tipo dei valori, ma questo ci porterebbe a non considerare l\u2019oggetto in query string come una Pizza<\/strong>, lasciando il PizzaReader<\/strong> a stato e pancia vuoti. E allora? Dobbiamo costruire una complessa funzione parser per ogni interfaccia della nostra applicazione?<\/p>\n\n\n\n No: esiste un modo pi\u00f9 semplice e sicuro, e viene dalla libreria Yup<\/strong><\/a>.<\/p>\n\n\n\n Se siete abituati a lavorare in React, molto probabilmente conoscerete gi\u00e0 Yup<\/strong>: \u00e8 una delle librerie pi\u00f9 diffuse per la validazione dei form, spesso usata insieme a Formik<\/strong><\/a>. Ma validare i form non \u00e8 l\u2019unica cosa di cui \u00e8 capace; per il nostro problema, in particolare, ci interessa il metodo cast<\/strong><\/a>. Si tratta di una funzionalit\u00e0 che permette, dato un valore<\/strong> che pu\u00f2 essere un oggetto, di tentare di estrapolare un secondo valore che rispetta uno specifico schema<\/strong>, proprio come quelli usati nella validazione dei form.<\/p>\n\n\n\n Installiamo Yup<\/strong> e le sue dichiarazioni di tipo\u2026<\/p>\n\n\n\n \u2026e, insieme all\u2019interfaccia, creiamo anche lo schema Yup<\/strong> per Pizza<\/strong>. <\/p>\n\n\n\n Infine, creiamo un terzo componente, PizzaTypedReader<\/strong>. La sua struttura sar\u00e0 identica al PizzaReader<\/strong>, se non per il fatto che user\u00e0 lo schema per il parsing del valore in query string.<\/p>\n\n\n\n Il metodo cast<\/strong> dello schema tenter\u00e0 di restituire un oggetto che rispetta la struttura dati definita. In questo caso, il valore string<\/strong> \u20181\u2019<\/strong> subir\u00e0 un casting nel number<\/strong> 1<\/strong>, dato che lo schema impone che il campo id<\/strong> sia di tipo number<\/strong>. Se l\u2019input non \u00e8 compatibile con lo schema, ad esempio perch\u00e9 id<\/strong> \u00e8 una stringa non numerica oppure perch\u00e9 manca un campo non opzionale, verr\u00e0 lanciato un errore. Nel momento in cui la chiamata ha successo, quindi, possiamo essere certi che newPizza<\/strong> sia una Pizza<\/strong>, con una piccola type assertion per convincere anche TypeScript della cosa<\/a>.<\/p>\n\n\n\n Aggiungiamo il terzo componente insieme agli altri\u2026<\/p>\n\n\n\n \u2026e proviamo di nuovo.<\/p>\n\n\n\n Adesso s\u00ec che ci siamo! Grazie a Yup<\/strong>, l\u2019ID di newPizza<\/strong> \u00e8 un number<\/strong>, e la strict comparison ha successo. Possiamo usare il metodo cast<\/strong> in qualunque situazione per assicurarci che i tipi a runtime siano quelli che ci aspettiamo nel codice: valori scalari, oggetti, array di oggetti, con ogni tipo di schema, non importa quanto complesso. Questo ci permette anche di validare la struttura di dati provenienti da fonti poco affidabili, ad esempio informazioni che possono essere facilmente modificate da utenti malintenzionati, come il localStorage<\/strong> del browser o, appunto, una query string. Occhio a non farci troppo affidamento, per\u00f2: questa validazione si limita al tipo, il contenuto effettivo \u00e8 tutta un\u2019altra storia!Spero che questa lettura possa essere stata utile. Se vi va, potete dare un\u2019occhiata alla repository del progetto<\/a>. Io credo che ordiner\u00f2 una pizza.<\/p>\n\n\n\n <\/p>\n\n\n\n
…oppure no?
JavaScript, si sa, \u00e8 un linguaggio dinamicamente tipato, e usare TypeScript<\/a> – come facciamo noi – per aiutarci a identificare errori di tipo prima che sia troppo tardi non toglie il fatto che a runtime la tipizzazione di variabili e propriet\u00e0 sia dinamica. Questo significa che, soprattutto quando si leggono dati da fonti puramente testuali, potremmo ottenere valori che non ci aspettiamo.<\/p>\n\n\n\nnpx create-react-app react18-typed-parsing --template typescript<\/pre>\n<\/div>\n\n\n\n
npm install qs react-router react-router-dom\nnpm install -D @types\/qs\n<\/pre>\n\n\n\n
\/\/ src\/models\/Pizza.ts\n\/\/ Interfaccia per la struttura dati pizza.\nexport interface Pizza {\n id: number\n\n name: string\n\n description?: string\n}\n<\/pre>\n\n\n\n\n
\/\/ src\/pages\/PizzaWrapper.tsx\nimport React, {ReactElement} from 'react';\nimport PizzaWriter from '..\/components\/PizzaWriter';\nimport PizzaReader from '..\/components\/PizzaReader';\n\nconst PizzaWrapper = (): ReactElement | null => {\n return (\n <>\n <PizzaWriter\/>\n <PizzaReader\/>\n <\/>\n );\n}\n\nexport default PizzaWrapper;\n<\/pre>\n\n\n\n\/\/ src\/App.tsx\nimport React from 'react';\nimport {\n createBrowserRouter,\n RouterProvider,\n} from \"react-router-dom\";\nimport '.\/App.css';\nimport PizzaWrapper from '.\/pages\/PizzaWrapper';\n\nconst router = createBrowserRouter([\n {\n path: '\/',\n element: <PizzaWrapper\/>,\n },\n]);\n\nfunction App() {\n return (\n <RouterProvider router={router}\/>\n );\n}\n\nexport default App;\n<\/pre>\n\n\n\n\/\/ src\/components\/PizzaWriter.tsx\nimport React, {ReactElement} from 'react';\nimport {useSearchParams} from 'react-router-dom';\nimport Qs from 'qs';\nimport {Pizza} from '..\/models\/Pizza';\n\n\/\/ La nostra pizza, da salvare in query string.\nconst pizzaToWrite: Pizza = {\n id: 1,\n name: 'Margherita',\n description: 'La classica!',\n};\n\nconst PizzaWriter = (): ReactElement => {\n \/\/ Metodo per modificare la query string.\n const [, setSearchParams] = useSearchParams();\n\n \/\/ Al click, la pizza viene salvata in query string.\n const savePizzaInQueryString = (): void => {\n setSearchParams(Qs.stringify(pizzaToWrite));\n }\n\n return (\n <div className={'querystring-writer'}>\n <h1>Query string writer<\/h1>\n <button onClick={savePizzaInQueryString}>\n Salva pizza in query string\n <\/button>\n <\/div>\n );\n}\n\nexport default PizzaWriter;\n<\/pre>\n\n\n\n\/\/ src\/components\/PizzaReader.tsx\nimport React, {ReactElement, useEffect, useState} from 'react';\nimport {useSearchParams} from 'react-router-dom';\nimport Qs from 'qs';\nimport {Pizza} from '..\/models\/Pizza';\n\n\/\/ La pizza che ci si aspetta di ricevere dalla query string.\nconst pizzaToRead = {\n id: 1,\n name: 'Margherita',\n description: 'La classica!',\n};\n\nconst PizzaReader = (): ReactElement | null => {\n const [searchParams] = useSearchParams();\n\n \/\/ La pizza che \u00e8 stata recuperata dalla query string.\n const [pizza, setPizza] =\n useState<Pizza | null>(null);\n\n useEffect(() => {\n \/\/ Parsing della pizza dalla query string.\n const pizzaRaw = Qs.parse(searchParams.toString());\n\n \/\/ TODO: e adesso?\n }, [searchParams]);\n\n \/\/ I dati della pizza vengono mostrati all'utente, se presenti.\n return (\n <div className={'querystring-reader'}>\n <h1>Query string reader<\/h1>\n {pizza !== null ? (\n <div className={'pizza-info'}>\n <div>\n <div className={'bold'}>ID<\/div>\n <div>{pizza.id}<\/div>\n <\/div>\n <div>\n <div className={'bold'}>Nome<\/div>\n <div>{pizza.name}<\/div>\n <\/div>\n <div>\n <div className={'bold'}>Descrizione<\/div>\n <div>{pizza.description}<\/div>\n <\/div>\n <div>\n {\/* Se la pizza \u00e8 una margherita, si mostra l'informazione, in base all'ID. *\/}\n <div className={'bold'}>Margherita<\/div>\n <div>{pizza.id === pizzaToRead.id ? 'S\u00ec' : 'No'}<\/div>\n <\/div>\n <\/div>\n ) : (\n 'Nessuna pizza in query string :('\n )}\n <\/div>\n );\n};\n\nexport default PizzaReader;\n\n<\/pre>\n\n\n\n\/\/ src\/helpers\/pizzaHelper.ts\nimport {Pizza} from '..\/models\/Pizza';\n\n\/\/ Type guard per verificare che un oggetto qualsiasi sia una pizza.\nexport const isPizza = (obj: any): obj is Pizza => {\n return 'id' in obj && 'name' in obj && 'description' in obj;\n}\n<\/pre>\n\n\n\nimport {isPizza} from '..\/helpers\/pizzaHelper';\n\n \u2026\n\n useEffect(() => {\n \/\/ Parsing della pizza dalla query string.\n const pizzaRaw = Qs.parse(searchParams.toString());\n\n \/\/ Se l'oggetto \u00e8 una pizza, lo si salva nello stato.\n if (isPizza(pizzaRaw)) {\n setPizza(pizzaRaw);\n }\n }, [searchParams]);\n<\/pre>\n\n\n\nnpm run start<\/pre>\n\n\n\n
<\/p>\n\n\n\n
<\/p>\n\n\n\n
<div>\n {\/* Se la pizza \u00e8 una margherita, si mostra l'informazione, in base all'ID. *\/}\n <div className={'bold'}>Margherita<\/div>\n <div>{pizza.id === pizzaToRead.id ? 'S\u00ec' : 'No'}<\/div>\n <\/div>\n<\/pre>\n\n\n\nnpm install yup\nnpm install -D @types\/yup\n<\/pre>\n\n\n\n
\/\/ src\/models\/Pizza.ts\n\u200b\u200bimport * as yup from 'yup';\n\n\/\/ Interfaccia per la struttura dati pizza.\nexport interface Pizza {\n id: number\n\n name: string\n\n description?: string\n}\n\n\/\/ Schema Yup per la struttura dati pizza.\nexport const pizzaSchema = yup.object({\n id: yup.number().required(),\n name: yup.string().required(),\n description: yup.string(),\n});\n<\/pre>\n\n\n\n\/\/ src\/components\/PizzaTypedReader.tsx\nimport React, {ReactElement, useEffect, useState} from 'react';\nimport {useSearchParams} from 'react-router-dom';\nimport Qs from 'qs';\nimport {Pizza, pizzaSchema} from '..\/models\/Pizza';\n\n\/\/ La pizza che ci si aspetta di ricevere dalla query string.\nconst pizzaToRead = {\n id: 1,\n name: 'Margherita',\n description: 'La classica!',\n};\n\nconst PizzaTypedReader = (): ReactElement | null => {\n\n \u2026\n\n useEffect(() => {\n \/\/ Parsing della pizza dalla query string.\n const pizzaRaw = Qs.parse(searchParams.toString());\n\n \/\/ Si usa Schema.cast di Yup per tentare di fare il parsing dell'oggetto.\n try {\n \/\/ Richiesta la type assertion as Pizza per evitare errori di tipo.\n const newPizza = pizzaSchema.cast(pizzaRaw) as Pizza;\n\n \/\/ L'oggetto \u00e8 una pizza.\n setPizza(newPizza);\n } catch (error) {\n \/\/ L'oggetto non \u00e8 una pizza.\n setPizza(null);\n }\n }, [searchParams]);\n\n \u2026\n};\n\nexport default PizzaTypedReader;\n<\/pre>\n\n\n\n\/\/ src\/pages\/PizzaWrapper.tsx\nimport React, {ReactElement} from 'react';\nimport PizzaWriter from '..\/components\/PizzaWriter';\nimport PizzaReader from '..\/components\/PizzaReader';\nimport PizzaTypedReader from '..\/components\/PizzaTypedReader';\n\nconst PizzaWrapper = (): ReactElement | null => {\n return (\n <>\n <PizzaWriter\/>\n <PizzaReader\/>\n <PizzaTypedReader\/>\n <\/>\n );\n}\n\nexport default PizzaWrapper;\n<\/pre>\n\n\n\n<\/p>\n\n\n\n