The post Parse di oggetti tipati con Yup cast first appeared on Oimmei Digital Consulting.
]]>C’è una grande quantità e varietà di fonti dati: API web, SDK, documenti sul file system, il localStorage del browser, una query string. Spesso, ad esempio, può capitare di dover reperire dati serializzati in forma testuale che magari noi stessi avevamo salvato da qualche parte per memorizzare una scelta dell’utente.
Leggere queste informazioni, che lo si faccia manualmente con gli strumenti nativi di JavaScript o che si usi una libreria, tipicamente è piuttosto semplice: si interroga la fonte dati, si ottiene un elemento o una lista ed è tutto pronto.
…oppure no?
JavaScript, si sa, è un linguaggio dinamicamente tipato, e usare TypeScript – 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à sia dinamica. Questo significa che, soprattutto quando si leggono dati da fonti puramente testuali, potremmo ottenere valori che non ci aspettiamo.
Tipicamente ciò avviene con campi numerici, ma può 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, per poi scoprire a runtime che in realtà si tratta di una stringa, ritrovandoci con bug subdoli e poco evidenti a prima vista. Vediamo un esempio pratico.
Immaginiamo una situazione che sarà sicuramente capitata a chiunque si trovi nello sviluppo web: stiamo creando un’applicazione React che salva in query string i dati di una deliziosa pizza, e in seguito li recupera per mostrarli all’utente.
Creiamo allora un nuovo progetto create-react-app, con TypeScript come piace a noi, e mettiamoci al lavoro.
npx create-react-app react18-typed-parsing --template typescript
Per serializzare e deserializzare gli oggetti useremo la libreria Qs, con react-router e react-router-dom per la manipolazione della query string, senza dimenticare le dichiarazioni TypeScript.
npm install qs react-router react-router-dom npm install -D @types/qs
Innanzitutto, creiamo un semplice modello dati per la nostra pizza, con un ID e un paio di campi testuali.
// src/models/Pizza.ts
// Interfaccia per la struttura dati pizza.
export interface Pizza {
id: number
name: string
description?: string
}
La nostra applicazione web avrà due componenti.
I componenti si troveranno all’interno del contenitore PizzaWrapper…
// src/pages/PizzaWrapper.tsx
import React, {ReactElement} from 'react';
import PizzaWriter from '../components/PizzaWriter';
import PizzaReader from '../components/PizzaReader';
const PizzaWrapper = (): ReactElement | null => {
return (
<>
<PizzaWriter/>
<PizzaReader/>
</>
);
}
export default PizzaWrapper;
…che sarà la root della navigazione.
// src/App.tsx
import React from 'react';
import {
createBrowserRouter,
RouterProvider,
} from "react-router-dom";
import './App.css';
import PizzaWrapper from './pages/PizzaWrapper';
const router = createBrowserRouter([
{
path: '/',
element: <PizzaWrapper/>,
},
]);
function App() {
return (
<RouterProvider router={router}/>
);
}
export default App;
PizzaWriter è semplice: alla pressione di un pulsante, serializza un oggetto pizza con Qs e salva il risultato in query string con l’hook di react-router-dom useSearchParams.
// src/components/PizzaWriter.tsx
import React, {ReactElement} from 'react';
import {useSearchParams} from 'react-router-dom';
import Qs from 'qs';
import {Pizza} from '../models/Pizza';
// La nostra pizza, da salvare in query string.
const pizzaToWrite: Pizza = {
id: 1,
name: 'Margherita',
description: 'La classica!',
};
const PizzaWriter = (): ReactElement => {
// Metodo per modificare la query string.
const [, setSearchParams] = useSearchParams();
// Al click, la pizza viene salvata in query string.
const savePizzaInQueryString = (): void => {
setSearchParams(Qs.stringify(pizzaToWrite));
}
return (
<div className={'querystring-writer'}>
<h1>Query string writer</h1>
<button onClick={savePizzaInQueryString}>
Salva pizza in query string
</button>
</div>
);
}
export default PizzaWriter;
PizzaReader è dove le cose iniziano a complicarsi un po’. Di base, quel che vogliamo è stare in ascolto sulla query string, sempre con useSearchParams, per essere pronti a ricevere una Pizza e metterla nello stato. Appena arriva, mostriamo all’utente i dati della Pizza. Ci aspettiamo di ricevere una margherita, quindi controlliamo anche, in base all’ID, che la pizza sia quella che abbiamo ordinato.
Ma come facciamo a essere sicuri che l’oggetto che leggiamo sia proprio una Pizza?
// src/components/PizzaReader.tsx
import React, {ReactElement, useEffect, useState} from 'react';
import {useSearchParams} from 'react-router-dom';
import Qs from 'qs';
import {Pizza} from '../models/Pizza';
// La pizza che ci si aspetta di ricevere dalla query string.
const pizzaToRead = {
id: 1,
name: 'Margherita',
description: 'La classica!',
};
const PizzaReader = (): ReactElement | null => {
const [searchParams] = useSearchParams();
// La pizza che è stata recuperata dalla query string.
const [pizza, setPizza] =
useState<Pizza | null>(null);
useEffect(() => {
// Parsing della pizza dalla query string.
const pizzaRaw = Qs.parse(searchParams.toString());
// TODO: e adesso?
}, [searchParams]);
// I dati della pizza vengono mostrati all'utente, se presenti.
return (
<div className={'querystring-reader'}>
<h1>Query string reader</h1>
{pizza !== null ? (
<div className={'pizza-info'}>
<div>
<div className={'bold'}>ID</div>
<div>{pizza.id}</div>
</div>
<div>
<div className={'bold'}>Nome</div>
<div>{pizza.name}</div>
</div>
<div>
<div className={'bold'}>Descrizione</div>
<div>{pizza.description}</div>
</div>
<div>
{/* Se la pizza è una margherita, si mostra l'informazione, in base all'ID. */}
<div className={'bold'}>Margherita</div>
<div>{pizza.id === pizzaToRead.id ? 'Sì' : 'No'}</div>
</div>
</div>
) : (
'Nessuna pizza in query string :('
)}
</div>
);
};
export default PizzaReader;
Una strada potrebbe essere quella di definire una type guard, per assicurarci che l’oggetto in query string abbia i campi che ci aspettiamo.
// src/helpers/pizzaHelper.ts
import {Pizza} from '../models/Pizza';
// Type guard per verificare che un oggetto qualsiasi sia una pizza.
export const isPizza = (obj: any): obj is Pizza => {
return 'id' in obj && 'name' in obj && 'description' in obj;
}
Proviamo a completare la useEffect di PizzaReader così.
import {isPizza} from '../helpers/pizzaHelper';
…
useEffect(() => {
// Parsing della pizza dalla query string.
const pizzaRaw = Qs.parse(searchParams.toString());
// Se l'oggetto è una pizza, lo si salva nello stato.
if (isPizza(pizzaRaw)) {
setPizza(pizzaRaw);
}
}, [searchParams]);
Dal punto di vista di TypeScript, è tutto a posto. Facciamo partire la nostra applicazione con…
npm run start
…e apriamo il browser, per vedere PizzaReader pronto a ricevere una Pizza.
Premiamo senza indugio il pulsante per consegnare il nostro stupendo pacchetto di amore e carboidrati, e vediamo come cambia la situazione.
A una prima occhiata è tutto a posto, ma qualcosa non va. I dati della Pizza sembrano corretti, se non per il fatto che PizzaReader non vede la Pizza come una margherita. Come mai?
La chiave è nel confronto che stiamo facendo sull’ID.
<div>
{/* Se la pizza è una margherita, si mostra l'informazione, in base all'ID. */}
<div className={'bold'}>Margherita</div>
<div>{pizza.id === pizzaToRead.id ? 'Sì' : 'No'}</div>
</div>
Il problema è che la pizzaToRead è stata definita nel nostro codice, rispettando l’interfaccia definita, mentre pizza viene recuperata dalla query string. Nel primo caso, l’ID viene correttamente impostato come un number; nel secondo, però, non avendo la query string nessuna indicazione sul tipo delle variabili, tutti i valori avranno tipo string a runtime. La strict equality che ci aspettiamo, dunque, non è rispettata: i tipi sono diversi, anche se TypeScript non può saperlo.
Risolvere questa situazione non è banale come può sembrare. Certo, per un caso così semplice potremmo usare la semplice equality, ma in situazioni più complesse? Se dovessimo usare un metodo specifico di String o di Number?
Si potrebbe pensare di rendere più stretta la type guard isPizza per controllare anche il tipo dei valori, ma questo ci porterebbe a non considerare l’oggetto in query string come una Pizza, lasciando il PizzaReader a stato e pancia vuoti. E allora? Dobbiamo costruire una complessa funzione parser per ogni interfaccia della nostra applicazione?
No: esiste un modo più semplice e sicuro, e viene dalla libreria Yup.
Se siete abituati a lavorare in React, molto probabilmente conoscerete già Yup: è una delle librerie più diffuse per la validazione dei form, spesso usata insieme a Formik. Ma validare i form non è l’unica cosa di cui è capace; per il nostro problema, in particolare, ci interessa il metodo cast. Si tratta di una funzionalità che permette, dato un valore che può essere un oggetto, di tentare di estrapolare un secondo valore che rispetta uno specifico schema, proprio come quelli usati nella validazione dei form.
Installiamo Yup e le sue dichiarazioni di tipo…
npm install yup npm install -D @types/yup
…e, insieme all’interfaccia, creiamo anche lo schema Yup per Pizza.
// src/models/Pizza.ts
import * as yup from 'yup';
// Interfaccia per la struttura dati pizza.
export interface Pizza {
id: number
name: string
description?: string
}
// Schema Yup per la struttura dati pizza.
export const pizzaSchema = yup.object({
id: yup.number().required(),
name: yup.string().required(),
description: yup.string(),
});
Infine, creiamo un terzo componente, PizzaTypedReader. La sua struttura sarà identica al PizzaReader, se non per il fatto che userà lo schema per il parsing del valore in query string.
// src/components/PizzaTypedReader.tsx
import React, {ReactElement, useEffect, useState} from 'react';
import {useSearchParams} from 'react-router-dom';
import Qs from 'qs';
import {Pizza, pizzaSchema} from '../models/Pizza';
// La pizza che ci si aspetta di ricevere dalla query string.
const pizzaToRead = {
id: 1,
name: 'Margherita',
description: 'La classica!',
};
const PizzaTypedReader = (): ReactElement | null => {
…
useEffect(() => {
// Parsing della pizza dalla query string.
const pizzaRaw = Qs.parse(searchParams.toString());
// Si usa Schema.cast di Yup per tentare di fare il parsing dell'oggetto.
try {
// Richiesta la type assertion as Pizza per evitare errori di tipo.
const newPizza = pizzaSchema.cast(pizzaRaw) as Pizza;
// L'oggetto è una pizza.
setPizza(newPizza);
} catch (error) {
// L'oggetto non è una pizza.
setPizza(null);
}
}, [searchParams]);
…
};
export default PizzaTypedReader;
Il metodo cast dello schema tenterà di restituire un oggetto che rispetta la struttura dati definita. In questo caso, il valore string ‘1’ subirà un casting nel number 1, dato che lo schema impone che il campo id sia di tipo number. Se l’input non è compatibile con lo schema, ad esempio perché id è una stringa non numerica oppure perché manca un campo non opzionale, verrà lanciato un errore. Nel momento in cui la chiamata ha successo, quindi, possiamo essere certi che newPizza sia una Pizza, con una piccola type assertion per convincere anche TypeScript della cosa.
Aggiungiamo il terzo componente insieme agli altri…
// src/pages/PizzaWrapper.tsx
import React, {ReactElement} from 'react';
import PizzaWriter from '../components/PizzaWriter';
import PizzaReader from '../components/PizzaReader';
import PizzaTypedReader from '../components/PizzaTypedReader';
const PizzaWrapper = (): ReactElement | null => {
return (
<>
<PizzaWriter/>
<PizzaReader/>
<PizzaTypedReader/>
</>
);
}
export default PizzaWrapper;
…e proviamo di nuovo.
Adesso sì che ci siamo! Grazie a Yup, l’ID di newPizza è un number, e la strict comparison ha successo.
Possiamo usare il metodo cast 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 del browser o, appunto, una query string. Occhio a non farci troppo affidamento, però: questa validazione si limita al tipo, il contenuto effettivo è tutta un’altra storia!Spero che questa lettura possa essere stata utile. Se vi va, potete dare un’occhiata alla repository del progetto. Io credo che ordinerò una pizza.
Foto di Lukas
The post Parse di oggetti tipati con Yup cast first appeared on Oimmei Digital Consulting.
]]>The post TypeScript: altri 5 trucchi per lo sviluppo first appeared on Oimmei Digital Consulting.
]]>Beh, noi sì. Parecchie volte, in effetti. Abbastanza da farmi chiedere se esista uno strumento migliore per sviluppare e mantenere certe applicazioni. Qualcosa che renda più semplice trovare errori con la gestione e l’utilizzo delle strutture dati, per esempio.
Per fortuna, la risposta è sì, e questo strumento è TypeScript! Estensione open source di JavaScript sviluppata principalmente da Microsoft, TypeScript aggiunge al linguaggio di programmazione preferito dei browser web molti nuovi strumenti e funzionalità, in particolare la tipizzazione statica, per rendere i progetti più robusti e facili da mantenere.
Molto probabilmente tutto questo lo sapete già, soprattutto se frequentate questo blog. Non solo per l’incredibile diffusione che ha raggiunto TypeScript negli ultimi anni, ma anche perché proprio su queste pagine era già uscito un mio articolo in cui presentavo alcune interessanti funzionalità di questo linguaggio. Avevo ipotizzato che prima o poi potesse uscire un sequel, e, puntuale come un errore “Cannot read property of undefined” in un’applicazione JavaScript, eccolo qua.
Ripartiamo subito, allora. Cinque paragrafi, cinque strumenti più o meno noti di TypeScript che potrebbero sorprendervi, con esempi e link al playground ufficiale per la versione 5.1.
Questo è molto semplice, ma credo comunque che per qualcuno sarà una novità.
Chiunque abbia lavorato con TypeScript sa come tipizzare staticamente gli array: è sufficiente definire il tipo del singolo elemento, e aggiungere le parentesi quadre dopo di esso.
const numberArray: number[] = [];
// Validi:
numberArray.push(1);
numberArray.push(2);
// Non valido:
numberArray.push("string");
La cosa non altrettanto ovvia è che è possibile utilizzare le definizioni di tipo anche per creare in modo molto semplice delle tuple – già menzionate nel primo articolo -, o ennuple se preferite, ovvero strutture dati formate da una combinazione ordinata di elementi. Ecco un esempio di combinazione formata da tre numeri.
const numberArray: number[] = [];
// Validi:
numberArray.push(1);
numberArray.push(2);
// Non valido:
numberArray.push("string");
Il vantaggio delle tuple è che, come si vede dallo snippet di codice, la tipizzazione statica del compilatore TypeScript andrà a validare anche la cardinalità degli elementi, e non soltanto il loro tipo, assicurandoci quindi che la struttura dati contenga sempre tutti e soli gli elementi che ci aspettiamo.
Le tuple hanno molte applicazioni nei progetti software. In React, ad esempio, sono molto utili per definire variabili di stato che raccolgono in una sola semplice struttura dati più valori strettamente legati fra loro, così da poterla leggere e aggiornare senza rendere lo stato del relativo componente troppo ingombrante e verboso, soprattutto con gli hook.
A questo proposito, vale la pena di specificare che le tuple possono contenere dati eterogenei di qualsiasi tipo, anche complessi – incluse altre tuple, se volete metterci un po’ di creatività!
interface ResponseBody {
title: string
content: string
}
// Questa tupla contiene un codice HTTP, un
// messaggio di risposta e il body di risposta.
const apiResponse: [number, string, ResponseBody] = [
200,
"success",
{
title: "Titolo",
content: "Contenuto",
},
];
// Recupero i singoli elementi tramite destructuring
// (https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/Destructuring_assignment).
const [statusCode, message, body] = apiResponse;
console.log(statusCode);
console.log(message);
console.log(body);
Utility type! Ve li ricordate? Avevo già presentato un paio di questi costrutti modificatori di tipo nell’articolo precedente. Oggi ne vediamo altri due, spesso ignorati ma non per questo poco utili.
Readonly è un utility type che, preso un tipo, lo trasforma nel suo corrispondente in sola lettura. Nel caso dell’interfaccia per un oggetto, ad esempio, questo utility type permette di modificare quella interfaccia in modo che nessuna delle proprietà dell’oggetto possa essere riassegnata dopo la creazione.
// Interfaccia per un oggetto con ID e titolo.
interface SomeInterface {
id: number
title: string
}
// Oggetto SomeInterface:
const someObject: SomeInterface = {
id: 1,
title: "Titolo",
};
// Oggetto SomeInterface in sola lettura:
const someReadonlyObject: Readonly<SomeInterface> = {
id: 2,
title: "Titolo in sola lettura",
};
// Valido:
someObject.title = "Altro titolo";
// Non valido:
someReadonlyObject = "Altro titolo ancora";
Questa funzionalità può sembrare relativamente inutile a prima vista, ma pensate a quante volte si ha a che fare con valori che dovrebbero restare immutabili: oggetti che contengono configurazioni di qualche tipo, lo stato di un componente React, lo stato di uno store Redux o tutte quelle librerie che si basano sulla comparazione standard di JavaScript per scatenare determinati effetti nell’applicazione. Grazie a Readonly, è il compilatore stesso che può aiutarvi a gestire questi valori nel modo corretto, lanciando un errore quando tentate di assegnare una variabile che non dovrebbe essere riassegnata. È come un const, ma che va più in profondità!
NonNullable è un utility type che esclude da uno union type i tipi null o undefined. Credo che questo sia abbastanza autoesplicativo: utile quando un certo tipo, come quello della proprietà di un’interfaccia, prevede la possibilità che un valore sia vuoto, ma abbiamo bisogno di inizializzare una nuova variabile che invece deve essere valorizzata.
interface SomeInterface {
// In questa interfaccia, value può essere vuoto.
value: number | null | undefined
}
// Inizializzo esternamente una variabile da usare come
// value, ma stavolta voglio che sia valorizzata.
// Valido:
const val1: SomeInterface["value"] = null;
// Non valido:
const val2: NonNullable<SomeInterface["value"]> = null;
// Valido:
const val3: NonNullable<SomeInterface["value"]> = 10;
Per chi ha utilizzato linguaggi di programmazione come Java o C# – noto anche come Microsoft Java -, i generics non hanno bisogno di presentazioni. Per chiunque invece avesse ancora un po’ della propria sanità mentale, i generics permettono di creare componenti software che possono accettare una varietà di tipi diversi, dove il tipo specifico sarà fornito da chi utilizza il componente stesso, pur mantenendo tutti i vantaggi della tipizzazione statica.
interface SomeInterface {
// In questa interfaccia, value può essere vuoto.
value: number | null | undefined
}
// Inizializzo esternamente una variabile da usare come
// value, ma stavolta voglio che sia valorizzata.
// Valido:
const val1: SomeInterface["value"] = null;
// Non valido:
const val2: NonNullable<SomeInterface["value"]> = null;
// Valido:
const val3: NonNullable<SomeInterface["value"]> = 10;
La documentazione ufficiale parla estensivamente dei generics, per cui vi consiglio di dare un’occhiata là se vi servisse un’introduzione più rigorosa. Qui mi limiterò a citare alcune interessanti funzionalità a tema.
Per esempio: i generics si possono rendere opzionali fornendo un valore di default nella dichiarazione del componente, come con i parametri di una funzione. Se nessun tipo è specificato quando il componente viene utilizzato, il compilatore prenderà quello di default.
// string è il tipo di default per questa interfaccia.
interface GenericInterface<T = string> {
param: T
}
// Validi:
const value1: GenericInterface = {
param: "stringa"
}
const value2: GenericInterface<number> = {
param: 42
}
// Non valido: TypeScript assume che param sia di tipo stringa.
const value3: GenericInterface = {
param: 42
}
Questa funzionalità può rivelarsi utile per introdurre l’uso dei generics in un componente che al momento non ne ha, oppure per aggiungerne di nuovi, senza intaccare la sua retrocompatibilità.
Attenzione però: specificare un tipo di default non significa mettere un vincolo a quali tipi possano essere specificati sul componente. Il tipo fornito al momento dell’uso, infatti, potrebbe anche essere completamente diverso da quello di default, come visto sopra con number e string.
Ecco perché sarebbe sbagliato scrivere questo.
// Un'interfaccia della nostra applicazione.
interface SomeInterface {
id: number
name: string
}
// Funzione che usa un tipo generico, con l'interfaccia come default.
function someFunction<T = SomeInterface>(param: T): void {
// Non valido: non c'è garanzia che T e SomeInterface saranno compatibili.
console.log(param.name);
}
Per queste situazioni, è possibile imporre un vincolo al tipo che potrà essere accettato dal componente con extends, che può anche essere combinato con un tipo di default.
// Un'interfaccia della nostra applicazione.
interface SomeInterface {
id: number
name: string
}
// Funzione che usa un tipo generico che estende SomeInterface.
function someFunction<T extends SomeInterface = SomeInterface>(
param: T
): void {
// Valido: qualsiasi tipo sia T, sarà
// un'estensione di SomeInterface.
console.log(param.name);
}
// Non valido: il tipo non è compatibile.
someFunction<number>(42);
Per casi particolarmente complessi, è possibile anche usare delle condizioni per modificare la tipizzazione statica di altre parti del componente a partire dai generics. Un esempio classico: mettiamo di avere un componente che gestisce un valore, e questo valore può essere un singolo elemento così come un array di elementi.
Con i generics, possiamo gestire il tutto con una sola interfaccia:
Ecco il risultato.
// Multiple vincolato su boolean, non multiplo come default.
interface ComponentProps<T, Multiple extends boolean = false> {
// Il valore è un singolo elemento oppure un array.
value: Multiple extends false ? T : T[]
// onChange ha come parametro un singolo elemento oppure un array.
onChange: (newValue: Multiple extends false ? T : T[]) => void
}
// Oggetto per il caso singolo (lascio il default su Multiple):
const singleValueComponentProps: ComponentProps<string> = {
value: "Sono una singola stringa!",
onChange: (newValue) => {
console.log(
"Questo log stamperà sempre TRUE:",
typeof newValue === "string"
);
},
};
// Oggetto per il caso multiplo:
const multipleValueComponentProps: ComponentProps<string, true> = {
value: ["Sono", "un", "array", "di", "stringhe", "ora!"],
onChange: (newValue) => {
console.log(
"Posso usare i metodi di un array, perché newValue è un array:"
);
newValue.forEach((currentValue) => console.log(currentValue));
},
}
Dopo il primo articolo, torniamo a parlare di type narrowing, questa volta facendo un po’ più di giustizia a questa importantissima funzionalità.
La scorsa volta, parlando delle discriminated union, avevo solo brevemente menzionato l’argomento del type narrowing, dicendo in genere che si tratta del meccanismo con cui TypeScript riesce a dedurre, dalla tipizzazione che definiamo e dal flusso della nostra applicazione, quale tipo avrà una specifica variabile a runtime, così da presentarci gli errori appropriati durante la compilazione. Quello che non avevo detto è quanto dannatamente potente e onnipresente sia questo meccanismo nelle applicazioni TypeScript, e quanto spesso lo utilizziamo senza nemmeno rendercene conto.
Pensiamo a una funzione con un parametro che può essere una stringa oppure un numero. Se riceviamo una stringa, vogliamo stampare la sua lunghezza; se invece riceviamo un numero, vogliamo stamparne il valore in notazione puntata. La nostra conoscenza di JavaScript ci porta naturalmente a scrivere del codice come questo, usando l’operatore typeof.
function someFunction (value: string | number): void {
if (typeof value === "string") {
// Stampo la lunghezza:
console.log("Lunghezza della stringa:", value.length);
} else {
// Stampo il valore:
console.log("Valore:", value.toFixed());
}
}
Molto semplice, vero? Eppure, diverse cose tutt’altro che banali stanno succedendo sotto la superficie di questo snippet.
All’inizio della funzione, abbiamo indicato che il parametro value può essere una stringa oppure un numero, tramite uno dei nostri adorabili union type. Ma, nei due rami dell’if, stiamo usando delle funzionalità non comuni a questi due tipi: nel ramo then usiamo length, proprietà che non esiste nel tipo number, mentre nel ramo else usiamo toFixed, metodo che non esiste nel tipo string.
Se proviamo a eliminare l’if, vediamo che entrambi i suoi rami restituiscono giustamente un errore.
function someFunction (value: string | number): void {
// Stampo la lunghezza:
console.log("Lunghezza della stringa:", value.length);
// Stampo il valore formattato:
console.log("Valore:", value.toFixed());
}
Nella prima versione, però, TypeScript non segnala nessuna anomalia. Cosa c’è sotto?
La risposta sta proprio nel type narrowing. TypeScript esamina il nostro codice e, dagli operatori che usiamo e dal flusso dell’applicazione, si rende conto che in determinati punti dell’esecuzione un certo valore avrà un tipo più specifico rispetto a quello che abbiamo dichiarato.
L’operatore typeof, ad esempio, costituisce quello che si chiama una type guard, ovvero un operatore di controllo speciale che ha effetto sul tipo che TypeScript deduce per un certo valore. Ecco perché la prima versione della funzione sopra non restituisce errori: partendo dallo union type string | number, TypeScript vede la condizione dell’if e capisce non soltanto che nel ramo then value sarà certamente di tipo string, ma anche che nel ramo else value sarà per esclusione di tipo number.
Il type narrowing è cruciale per le applicazioni TypeScript, e si applica a un gran numero di costrutti diversi: i typeof, ma anche i confronti, gli assegnamenti, operatori come in e instanceof, negli if, negli switch…
Ma voi non siete qui per lunghe spiegazioni sul funzionamento di TypeScript. Voi volete qualche dritta su quelle funzionalità che potete usare nel vostro codice e raccontare alle feste per ottenere credito, rispetto e ammirazione dai vostri amici, e io vi accontento subito.
(Nota: la conoscenza di quanto spiegato di seguito potrebbe non farvi effettivamente ottenere credito, rispetto e ammirazione dai vostri amici. L’autore dell’articolo declina ogni responsabilità riguardo la scarsa riuscita della vostra vita sociale.)
Ci sono molti casi in cui sfruttare il type narrowing non è così banale. Certo, finché si parla di tipi primitivi come stringhe e numeri, oppure di utility built-in come Date, allora è tutto molto semplice, ma… se ci fosse bisogno di lavorare con interfacce definite da noi?
Tenete a mente che le interfacce TypeScript hanno un piccolo grande problema: non esistono a runtime. In effetti, non esistono e basta, dato che le interfacce al momento non esistono in JavaScript. Sono un aiuto per il compilatore per rilevare errori di tipo statici, ma vengono eliminate nel corso della compilazione. Quindi no, non possiamo semplicemente usare instanceof come faremmo con le interfacce di altri linguaggi orientati agli oggetti; dobbiamo metterci un po’ di impegno in più.
Un caso tipico: abbiamo un’interfaccia SomeInterface, una seconda interfaccia SomeExtension che estende la prima e una funzione che prende oggetti di tipo SomeInterface. Se l’oggetto che riceviamo ha tipo SomeExtension, vogliamo fare delle operazioni supplementari all’interno della funzione. Peccato che l’operatore in non basti a convincere TypeScript delle nostre buone intenzioni.
interface SomeInterface {
id: number
title: string
}
interface SomeExtension extends SomeInterface {
description: string
content: string
otherFields: Record<string, any>
}
function someFunction (obj: SomeInterface): void {
// Stampo tutte le proprietà.
// Validi:
console.log(obj.id);
console.log(obj.title);
if ("description" in obj) {
// L'operatore in ci permette di accedere a description...
console.log(obj.description);
// ...ma non basta a far riconoscere obj
// come oggetto di tipo SomeExtension.
console.log(obj.content);
console.log(obj.otherFields);
}
}
(Fino a TypeScript 4.8, avremmo avuto un errore anche per l’accesso a description. Nella versione 4.9 c’è stata qualche modifica al type narrowing, che permette di accedere in sicurezza al campo specificamente testato con in, anche se il tipo dedotto sarà unknown.)
Quello che dobbiamo fare qui è far capire a TypeScript che il nostro accesso alle proprietà dell’estensione è giustificato, o, se preferite, che stiamo usando queste proprietà solo quando obj è effettivamente di tipo SomeExtension. Per fortuna, questo è possibile con i type predicate.
In sostanza, i type predicate ci permettono di definire delle type guard personalizzate, con condizioni arbitrariamente complesse, che garantiscono a TypeScript che in un certo blocco della nostra applicazione una determinata variabile sia di un certo tipo – proprio come l’operatore typeof, ma con una logica interamente definita da noi.
Creiamo una funzione isSomeExtension, che prende in ingresso un parametro di tipo SomeInterface e restituisce un type predicate che stabilisce che il parametro in ingresso è di tipo SomeExtension. Nel body, la funzione deve esaminare il parametro e restituire true se il predicato in uscita è valido, false altrimenti.
Infine, usiamo quella funzione nella condizione di someFunction.
interface SomeInterface {
id: number
title: string
}
interface SomeExtension extends SomeInterface {
description: string
content: string
otherFields: Record<string, any>
}
function isSomeExtension (value: SomeInterface): value is SomeExtension {
// Se value contiene description, è di tipo SomeExtension.
return "description" in value;
}
function someFunction (obj: SomeInterface): void {
// Stampo tutte le proprietà.
// Validi:
console.log(obj.id);
console.log(obj.title);
if (isSomeExtension(obj)) {
// Validi: obj è di tipo SomeExtension in questo punto del codice.
console.log(obj.description);
console.log(obj.content);
console.log(obj.otherFields);
}
}
Tutto corretto!
Un appunto importante in chiusura di questo lungo paragrafo: tenete conto che con i type predicate stiamo praticamente “saltando” i controlli di tipo che TypeScript ci offre, e che il compilatore si fiderà completamente di noi per quanto riguarda il funzionamento della type guard. Ciò significa che dobbiamo fare molta attenzione a scrivere la funzione che controlla il tipo: se sbagliamo qualche condizione nel body, ce ne renderemo conto solo dai bug a runtime!
Chi proviene da linguaggi come Java conoscerà il method overloading: si tratta di quella funzionalità che permette di specificare, all’interno di una classe o di un’interfaccia, più metodi con lo stesso nome e diversi insiemi di parametri – per esempio un numero diverso di parametri, oppure parametri di tipo differente.
Forse non tutti sanno che
una funzionalità molto simile esiste anche in TypeScript, e può essere davvero utile in certe situazioni. Sto parlando del function overload, utilizzabile sia sulle funzioni che sui metodi di una classe.
Il funzionamento è sostanzialmente lo stesso del method overloading, ma con una differenza importante: mentre il method overloading permette di dichiarare effettivamente più metodi con diversi body, nel function overload l’effettiva funzione con il body deve essere una sola, ma possono essere specificate diverse signature per le varie versioni. Ciò significa che la funzione dovrà essere scritta in modo da essere compatibile con tutte le signature, altrimenti avremo un errore di tipo.
// Signature diverse:
function someFunction (param: number): number;
function someFunction (param: string): string;
// Implementazione della funzione:
function someFunction (
param: number | string
): number | string {
if (typeof param === "number") {
console.log("È un numero!");
} else {
console.log("È una stringa!");
}
return param;
}
const val1 = someFunction(42);
const val2 = someFunction(
"Addio, e grazie per tutto il pesce"
);
Questo vincolo può far sembrare il function overload limitante e di scarsa utilità, ma bisogna tenere conto di un vantaggio importante: dichiarando la funzione in questo modo, TypeScript sarà in grado di assegnare correttamente, a seconda dei parametri in ingresso, il tipo del valore che viene restituito. In altre parole, non dovremo preoccuparci di fare manualmente un ulteriore type narrowing su quello che ci restituisce la nostra funzione.
Per dimostrare le potenzialità di quanto detto, aggiungiamo un paio di righe allo snippet di prima.
// Signature diverse:
function someFunction (param: number): number;
function someFunction (param: string): string;
// Implementazione della funzione:
function someFunction (
param: number | string
): number | string {
if (typeof param === "number") {
console.log("È un numero!");
} else {
console.log("È una stringa!");
}
return param;
}
const val1 = someFunction(42);
const val2 = someFunction(
"Addio, e grazie per tutto il pesce"
);
// Validi:
console.log(val1.toFixed());
console.log(val2.length);
In questo esempio, trattiamo val1 e val2 rispettivamente come un numero e una stringa. Il motivo per cui possiamo farlo senza ottenere errori è proprio il fatto che abbiamo utilizzato il function overload: TypeScript è in grado di dire che someFunction restituirà un numero quando riceve un numero e una stringa quando riceve una stringa.
Ecco come sarebbero cambiate le cose se non avessimo usato il function overload.
function someFunction (
param: number | string
): number | string {
if (typeof param === "number") {
console.log("È un numero!");
} else {
console.log("È una stringa!");
}
return param;
}
const val1 = someFunction(42);
const val2 = someFunction(
"Addio, e grazie per tutto il pesce"
);
// Non validi: TypeScript non è in grado
// di dedurre il valore delle variabili.
console.log(val1.toFixed());
console.log(val2.length);
// Dobbiamo fare type narrowing manualmente:
if (typeof val1 === "number") {
console.log(val1.toFixed);
}
if (typeof val2 === "string") {
console.log(val2.length);
}
Già questo semplicissimo esempio è diventato molto più verboso; potete immaginare l’impatto che può avere la cosa in un codice più complesso e realistico.
Un’applicazione molto valida per il function overload è quando abbiamo una funzione che può operare indifferentemente su un singolo valore o su un array di valori.
function multiplyValue(
value: number,
multiplyBy: number,
): number;
function multiplyValue(
value: number[],
multiplyBy: number,
): number[];
// Moltiplica un valore o ogni valore
// in un array per un operando.
function multiplyValue(
value: number | number[],
multiplyBy: number,
): number | number[] {
if (Array.isArray(value)) {
const result = [];
for (let i = 0; i < value.length; i++) {
result.push(value[i] * multiplyBy);
}
return result;
} else {
return value * multiplyBy;
}
}
const singleValue = multiplyValue(11, 2);
const arrayOfValues = multiplyValue(
[1, 1, 2, 3, 5],
5
);
// singleValue è un number:
console.log(singleValue.toFixed());
// arrayOfValues è un array di number:
arrayOfValues.forEach((item) => {
console.log(item.toFixed())
});
(Notare fra l’altro come anche Array.isArray sia una type guard valida.)
Nessuna type guard necessaria al di fuori della funzione! Esistono forse parole più dolci di “posso scrivere meno codice”?
Anche questo secondo articolo sulle funzionalità di TypeScript è finito (non hai ancora letto la prima parte? Che aspetti?!). Spero che questi paragrafi siano riusciti a farvi scoprire qualcosa di nuovo, o magari a farvi soffermare su qualche aspetto del linguaggio che non avevate mai considerato prima.
Di nuovo, se avete qualcuno dei vostri strumenti segreti per TypeScript che vi piacerebbe condividere, non vedo l’ora di conoscerli. Uscirà mai un terzo articolo? Chissà!
Foto di Christopher Gower su Unsplash
The post TypeScript: altri 5 trucchi per lo sviluppo first appeared on Oimmei Digital Consulting.
]]>The post TypeScript: 5 trucchi per lo sviluppo first appeared on Oimmei Digital Consulting.
]]>Ah, TypeScript. Il linguaggio di programmazione open source sviluppato da Microsoft come estensione di JavaScript ormai non ha più bisogno di presentazioni. Con la crescente popolarità di tecnologie come React, Angular e Node.js, la necessità di sviluppare applicativi JavaScript robusti e facili da mantenere si è fatta sempre più difficile da ignorare, e la tipizzazione statica fornita da TypeScript dà sicuramente una grossa mano per questo.
Se fate parte del sempre più ristretto gruppo di sviluppatori e sviluppatrici web che non hanno ancora adottato TypeScript, non preoccupatevi: per chi conosce JavaScript, il suo cugino staticamente tipato non è difficile da approcciare. Essendo un’estensione, TypeScript può nativamente compilare qualunque applicazione scritta in JavaScript – con l’eccezione di alcuni errori di tipo -, permettendo anche di passare progressivamente alle sue nuove funzionalità.
Lo scopo di questo articolo, però, non è introdurre TypeScript, anche perché ciò sarebbe ridondante per una buona parte di voi, bensì mostrare alcune interessanti funzionalità che ho pian piano scoperto nel corso del mio lavoro con il linguaggio diretto da Anders Hejlsberg. Al di là delle meccaniche più palesi, infatti, TypeScript ha molte possibilità non ovvie anche per chi ha una discreta esperienza con il suo utilizzo, e che possono risultare veramente molto utili per rendere le nostre applicazioni ancora più robuste e leggibili oppure per risparmiare codice non necessario.
Ecco quindi cinque trucchi da usare nei vostri progetti TypeScript, a partire dai più noti e diffusi fino ad arrivare a quelli un po’ più oscuri e situazionali, completi di esempi sul playground ufficiale, tutti garantiti sull’attuale versione del linguaggio (4.8).
Gli utility type sono tipi speciali disponibili globalmente in qualsiasi applicativo TypeScript. Si tratta di costrutti che, partendo da un tipo, ad esempio un’interfaccia, possono produrne un secondo modificando in qualche maniera il primo.
Esistono molti utility type e sono ben documentati nella guida ufficiale, quindi in questo articolo mi limiterò a illustrare due fra quelli che ho trovato più utili nei nostri progetti.
Omit è un utility type che, preso un tipo A con una serie di proprietà, lo trasforma in un tipo B rimuovendo tutte le proprietà specificate nella definizione.
// Tipo originale:
interface TypeA {
id: string
name: string
description: string
price: number
}
// Creo un nuovo tipo dall'originale, escludendo le proprietà description e price:
type TypeB = Omit<TypeA, "description" | "price">;
// Creo un oggetto del nuovo tipo:
const objB: TypeB = {
id: "1001", // Valido.
name: "Nome", // Valido.
description: "Descrizione", // Non valido: questa proprietà non esiste in TypeB.
}
Omit è molto utile in situazioni in cui si vogliono costruire oggetti parziali di un certo tipo, ma mantenendo i vincoli imposti dal tipo stesso, come le proprietà obbligatorie. Per questo, può rivelarsi più robusto e preciso di Partial, che invece si limita a rendere opzionali tutte le proprietà, anche se meno immediato.
Altro campo in cui Omit aiuta molto è il riutilizzo del codice. Immaginate questo scenario: nella vostra applicazione, magari in una libreria di terze parti, esiste già un tipo TextInputProps, che è un’interfaccia per le proprietà di un input testuale. Volete creare un nuovo input, identico al primo, ma che gestisce solo numeri e non stringhe. Si potrebbe pensare di estendere TextInputProps per cambiare il tipo di value, ma…
interface TextInputProps {
label: string
name: string
value: string
}
interface NumberInputProps extends TextInputProps {
value: number
}
Oh, no! Non possiamo estendere l’interfaccia perché i tipi di value non sono compatibili! E adesso? Dobbiamo per forza replicare l’interfaccia, oppure dividere la prima per separare i campi in comune?
No: Omit viene in nostro soccorso. Invece di estendere TextInputProps, estenderemo una versione trasformata, che ha tutti i campi tranne value. Questo ci permette di ridefinire senza problemi la proprietà, anche senza rispettare i vincoli dell’interfaccia madre. La nuova interfaccia non sarà più un’estensione della prima in termini di programmazione orientata agli oggetti – in altre parole: oggetti di tipo NumberInputProps non saranno anche di tipo TextInputProps -, ma in questo caso la cosa non ci interessa.
interface TextInputProps {
label: string
name: string
value: string
}
interface NumberInputProps extends Omit<TextInputProps, "value"> {
value: number
}
Una cosa in meno da fare prima di tornare a casa a giocare alla PlayStation! Woo-ooh!
Come avrete intuito, Pick ha il funzionamento diametralmente opposto, ovvero permette di costruire un secondo tipo prendendo soltanto un sottoinsieme delle proprietà del primo, e si rivela utile allo stesso modo.
// Tipo originale:
interface TypeA {
id: string
name: string
description: string
price: number
}
// Creo un nuovo tipo dall'originale, prendendo solo le proprietà id e name:
type TypeB = Pick<TypeA, "id" | "name">;
// Creo un oggetto del nuovo tipo:
const objB: TypeB = {
id: "1001", // Valido.
name: "Nome", // Valido.
description: "Descrizione", // Non valido: questa proprietà non esiste in TypeB.
}
(Nota per i so-tutto-io che si stavano già scrocchiando le dita: sì, so che la prima situazione si risolverebbe molto meglio con i generics type, era solo un esempio. Tenete giù le mani dalla tastiera e stasera non vi umilierò in Elden Ring.)
Questa è molto semplice, ma riesce comunque a evitare di replicare alcune informazioni nel codice.
Ecco una situazione abbastanza comune in un’applicazione TypeScript relativamente complessa: immaginate di avere un’interfaccia, con una serie di proprietà ciascuna con il proprio tipo. Dovete creare un oggetto che implementa questa interfaccia, ma uno dei valori è particolarmente complicato da calcolare, e preferite farlo prima in una variabile per poi usare questa variabile come valore. La domanda è: come fare a rendere questa variabile staticamente tipata con il tipo giusto?
La risposta più semplice è banalmente prendere il tipo della relativa proprietà dell’interfaccia e ripeterlo all’inizializzazione della variabile.
// Interfaccia:
interface SomeInterface {
id: number
code: string
name: string
}
// Dichiaro una variabile per calcolare il valore di code:
let code: string; // <- Tipo della proprietà ripetuto
// Logica di inizializzazione incredibilmente complessa:
code = "CODICE";
const someObject: SomeInterface = {
id: 1,
code: code,
name: "Nome",
}
Questa replicazione, però, non è necessaria. Quando abbiamo un tipo che ha delle proprietà, come un’interfaccia, TypeScript ci permette di fare riferimento direttamente al tipo di una specifica proprietà usando le parentesi quadre, come se stessimo accedendo a un comune oggetto JavaScript.
// Interfaccia:
interface SomeInterface {
id: number
code: string
name: string
}
// Dichiaro una variabile per calcolare il valore di code:
let code: SomeInterface["code"]; // <- Nessuna replicazione!
// Logica di inizializzazione incredibilmente complessa:
code = "CODICE";
const someObject: SomeInterface = {
id: 1,
code: code,
name: "Nome",
}
Solo con le parentesi quadre, però: non provateci con la notazione puntata.
Così facendo, sarà l’interfaccia la sola ad avere l’informazione sul tipo della proprietà, e se quest’ultimo in futuro cambiasse avremmo una cosa in meno da modificare. Intendiamoci: se la proprietà passa da un primo tipo a un secondo non compatibile dovremo in ogni caso correggere l’inizializzazione del valore, ma non avremo da preoccuparci della dichiarazione della variabile.
Allo stesso modo, è possibile accedere al tipo di un elemento di un array usando il tipo del suo indice, ovvero number.
// Interfaccia:
interface SomeInterface {
id: number
code: string
name: string
}
// Dichiaro un array di oggetti di tipo SomeInterface:
type SomeInterfaceArray = SomeInterface[];
// Inizializzo una variabile, usando l'array per prendere il tipo:
const someObject: SomeInterfaceArray[number] = {
id: 1,
code: "CODICE",
name: "Nome",
}
Una delle mie funzionalità preferite, e una davvero interessante per chi proviene dai classici linguaggi di programmazione orientati agli oggetti.
Tanto per cominciare: non credo che gli union type siano un mistero per nessuno di noi. Si tratta di quella funzionalità che permette di costruire un nuovo tipo combinando tipi esistenti con l’operatore pipe (|).
// Union type: type SomeUnion = string | boolean; // Valido: const someVariable1: SomeUnion = "Stringa"; // Valido: const someVariable2: SomeUnion = false; // Non valido: const someVariable3: SomeUnion = 10;
Altrettanto noto è il fatto che, dato che i valori scalari in TypeScript sono considerati tipi validi, è possibile costruire degli union type che permettano di valorizzare una variabile o una proprietà con uno di una serie di valori imposti dal tipo stesso, ad esempio una fra tre stringhe.
// Union type: type SomeUnion = "stringa_1" | "stringa_2" | "stringa_3"; // Valido: const someVariable1: SomeUnion = "stringa_1"; // Non valido: const someVariable2: SomeUnion = "stringa_5";
Tutto abbastanza noioso fin qui, ma datemi un attimo; ora arriva il bello. Sapevate che è possibile usare gli union type per discriminare fra diversi tipi e fare sì che il compilatore riconosca la tipizzazione statica? Un po’ contorto da spiegare a parole, lo so. Facciamo di nuovo un esempio.
Mettiamo che il nostro progetto TypeScript chiami o esponga un’API REST. La risposta di questa API contiene:
Come modelliamo questa struttura dati nella nostra applicazione?
Pensando alla tipica tecnica object-oriented, potremmo utilizzare un’interfaccia del genere, con status che può assumere uno dei due valori e error_message sempre presente ma opzionale.
interface ApiResponse {
status: "SUCCESS" | "FAILURE"
data: any
// Verrà popolata solo in caso di status FAILURE.
error_message?: string
}
Lo svantaggio di questa interfaccia è che non fornisce nessuna validazione statica su error_message. Siamo noi che dobbiamo assicurarci che il campo sia popolato nella giusta situazione, e possiamo accorgerci di eventuali errori solo in fase di debug.
interface ApiResponse {
status: "SUCCESS" | "FAILURE"
data: any
// Verrà popolata solo in caso di status FAILURE.
error_message?: string
}
// Valido: il compilatore non può dedurre che questa situazione è errata.
const response: ApiResponse = {
status: "SUCCESS",
data: {},
error_message: "Messaggio di errore"
}
Gli union type di TypeScript, tuttavia, ci mettono a disposizione una potente alternativa per risolvere il problema: la discriminated union, che fa parte della più ampia tematica del type narrowing, ovvero la funzionalità che permette a TypeScript di capire il tipo di una certa variabile in base alla nostra gestione della tipizzazione e al flusso dell’applicazione. Riscriviamo l’esempio precedente in questo modo.
Ecco il risultato.
interface ApiSuccessResponse {
status: "SUCCESS"
data: any
}
interface ApiFailureResponse {
status: "FAIL"
data: any
error_message: string
}
type ApiResponse = ApiSuccessResponse | ApiFailureResponse;
Con questa tipizzazione, il compilatore è in grado di discriminare fra i due diversi tipi di risposta, e riuscirà a validare staticamente la presenza e il tipo di error_message, sia in creazione che in lettura.
interface ApiSuccessResponse {
status: "SUCCESS"
data: any
}
interface ApiFailureResponse {
status: "FAIL"
data: any
error_message: string
}
type ApiResponse = ApiSuccessResponse | ApiFailureResponse;
// Non valido: in caso di status SUCCESS non può esserci error_message.
const response1: ApiResponse = {
status: "SUCCESS",
data: {},
error_message: "Messaggio di errore",
}
// Non valido: in caso di status FAIL deve esserci error_message.
const response2: ApiResponse = {
status: "FAIL",
data: {},
}
// Validi:
const response3: ApiResponse = {
status: "SUCCESS",
data: {},
}
const response4: ApiResponse = {
status: "FAIL",
data: {},
error_message: "Messaggio di errore",
}
Rassicurante, non è vero?
Per quanto siano comodi gli union type, hanno un difetto: essendo parte della tipizzazione statica, le informazioni sui possibili valori non possono in alcun modo essere utilizzate a runtime. Questo significa, per esempio, che non c’è modo di iterare sui possibili valori di uno union type, né di controllare se un certo valore ottenuto dinamicamente sia valido.
Il modo più pulito per gestire situazioni in cui c’è bisogno di usare una enumerata di valori sia staticamente che a runtime è – poco sorprendentemente – usare una Enum. Le Enum sono tipi che permettono di definire una serie di costanti con un nome, vengono utilizzati dal compilatore per la tipizzazione statica e, a differenza degli union type, esistono anche a runtime.
Un risultato simile, però, si può ottenere anche con un array di scalari, come un array di stringhe. Per farlo, occorre definire l’array con un costrutto chiamato const assertion. Di che cosa si tratta?
Quando una variabile viene dichiarata e inizializzata senza un’esplicita tipizzazione, TypeScript deduce per noi il tipo più adeguato a seconda del valore con cui è stata inizializzata. Un array di stringhe, quindi, riceverà il tipo string[].
const array = ["stringa_1", "stringa_2", "stringa_3"]; // Tipo dedotto: // declare const array: string[];
Questo comportamento può essere controllato con la const assertion, che impone a TypeScript di dedurre il tipo più specifico possibile a partire dal valore. Un array di stringhe inizializzato con una serie di valori, di conseguenza, verrà interpretato come un array in sola lettura, di lunghezza fissa, contenente solo quei valori in quelle specifiche posizioni – una tupla, insomma.
const array = ["stringa_1", "stringa_2", "stringa_3"] as const; // Tipo dedotto: // declare const array: readonly ["stringa_1", "stringa_2", "stringa_3"]
L’array può comunque essere utilizzato in lettura nel codice, per iterare sui valori oppure controllare se un altro valore compare nella lista, ma con questo ulteriore accorgimento può anche essere utilizzato per creare degli union type.
const someArray = ["stringa_1", "stringa_2", "stringa_3"] as const;
type SomeUnion = typeof someArray[number];
// Valido:
const someVar1: SomeUnion = "stringa_1";
// Non valido:
const someVar2: SomeUnion = "stringa_5";
// Posso comunque usare someArray come array:
if (someArray.indexOf("stringa_1") !== -1) {
console.log("Tipo valido");
}
for (let i = 0; i < someArray.length; i++) {
console.log(someArray[i]);
}
Questo ci permette di combinare i vantaggi dell’avere tutti i valori che ci interessano in un array, molto utile per esempio nella validazione Yup, e dell’avere uno union type per controllare staticamente il valore di una proprietà e avvalerci dell’autocompletamento del nostro IDE.
Wow, ancora un altro trucco sugli union type! Quante possibilità c’erano?
Come già detto sopra, in molti casi gli union type sono utilizzati per creare variabili o proprietà che possono assumere uno di una serie di valori scalari.
// Union type: type SomeUnion = "stringa_1" | "stringa_2" | "stringa_3"; // Valido: const someVariable1: SomeUnion = "stringa_1"; // Non valido: const someVariable2: SomeUnion = "stringa_5";
Questa funzionalità è comoda, ma anche abbastanza limitante, dato che non possiamo introdurre nessuna variazione in questi valori. Tutto ciò che non rientra precisamente negli scalari specificati nella definizione del tipo verrà rifiutato dal compilatore senza tanti complimenti.
Ora, mettiamo di avere una situazione in cui una stringa può assumere una serie di valori simili, ma non completamente statici. Per esempio, abbiamo un algoritmo che fa diversi tentativi per completare un task che potrebbe fallire, e vogliamo tenere una variabile che rappresenta lo stato di questo algoritmo.
Creiamo quindi una variabile status, che può valere:
Per come siamo abituati a usare gli union type, il meglio che potremmo fare è riportare i valori fissi e lasciare un generico string per l’ultimo…
// Union type per lo status: type StatusType = "none" | "success" | "fail" | string; // Questo è valido... const status1: StatusType = "none"; const status2: StatusType = "attempt_3"; // ...ma anche questo è valido. const status3: StatusType = "Ehi, ma questa stringa non contiene uno stato!";
…ma non è l’unica possibilità. Le stringhe con cui formiamo gli union type possono infatti avere parti dinamiche, che possiamo gestire semplicemente concatenando il tipo della parte dinamica alla parte fissa con le template string.
// Union type per lo status:
type StatusType = "none" | "success" | "fail" | `attempt_${number}`;
// Questi sono validi...
const status1: StatusType = "none";
const status2: StatusType = "attempt_3";
// ...e questo no.
const status3: StatusType = "Oh :(";
Vi vogliamo bene, union type <3
Approcciare TypeScript è un’esperienza stimolante per chi proviene da altri linguaggi di programmazione, soprattutto quelli orientati agli oggetti. La sua natura di estensione di un linguaggio non staticamente tipato come JavaScript ha permesso al team di sviluppo di trovare svariate soluzioni a problemi più o meno comuni, e anche dopo diverso tempo capita di scoprirne qualcuna nuova di tanto in tanto.
Vi ho presentato alcune delle funzionalità che ho trovato più interessanti nel corso del mio lavoro, e sono certo che ne scoprirò ancora altre man mano che vado avanti.
Voi che ne pensate? Conoscevate tutti i trucchi presentati nell’articolo, o sono riuscito a farvi scoprire qualcosa di nuovo? Ne conoscete altri che meritano di essere mostrati al mondo? Fatemelo sapere, magari un giorno o l’altro ci sarà un sequel!
Foto di Pakata Goh su Unsplash
The post TypeScript: 5 trucchi per lo sviluppo first appeared on Oimmei Digital Consulting.
]]>The post E-commerce: guida alla creazione di un modulo PrestaShop first appeared on Oimmei Digital Consulting.
]]>Fin qui tutto ok, ma che succede nel caso in cui volessimo aggiungere qualche piccola nuova funzionalità, come ad esempio mostrare un avviso configurabile nell’homepage del nostro e-commerce?
È qui che ci viene in soccorso il sistema di moduli di PrestaShop. Grazie ai moduli è possibile infatti aggiungere nuove funzionalità più o meno complesse al nostro negozio online.
In questo articolo vi spiegherò come creare il vostro primo modulo PrestaShop con un semplice esempio. Andremo a creare un modulo che mostra un avviso personalizzabile all’interno dell’homepage del vostro sito web e-commerce.
La struttura di PrestaShop prevede che i moduli debbano trovarsi all’interno della directory modules, che si trova nella cartella principale del sito web.
La prima cosa da fare è creare una nuova cartella all’interno della directory modules che chiameremo homepagebanner. Il nome della directory del modulo deve seguire alcune semplici regole per garantire il suo corretto funzionamento: deve avere lo stesso nome che vogliamo dare al modulo, non contenere spazi, solo caratteri alfanumerici (minuscoli), il trattino o l’underscore. Questa directory sarà il contenitore dei file necessari al funzionamento del modulo.
Creiamo quindi un nuovo file all’interno della directory, con lo stesso nome della cartella, nel nostro caso homepagebanner.php.
Questo file PHP conterrà la classe principale del modulo oltre ad eventuali altre classi.
Come possiamo vedere dal codice sottostante, il file deve iniziare con il controllo dell’esistenza della costante di versione PrestaShop, per prevenire che il file sia caricato direttamente da utenti malintenzionati.
Subito dopo abbiamo la definizione della classe principale, il cui nome deve essere lo stesso del modulo in CamelCase e deve estendere la classe Module.
All’interno del metodo costruttore della classe, andiamo a definire una serie di caratteristiche di base del modulo oltre a chiamare il costruttore della classe padre.
Alcune delle caratteristiche sono ad esempio il nome (che deve essere uguale al nome del modulo), la versione, l’autore, il range di conformità di versione, l’autore, il messaggio di conferma per la disinstallazione del modulo ecc.
<?php
if (!defined('_PS_VERSION_')) {
exit;
}
class HomepageBanner extends Module
{
public function __construct()
{
$this->name = 'homepagebanner';
$this->tab = 'front_office_features';
$this->version = '1.0.0';
$this->author = 'Duccio Bottai';
$this->need_instance = 0;
$this->ps_versions_compliancy = [
'min' => '1.7',
'max' => _PS_VERSION_
];
$this->bootstrap = true;
parent::__construct();
$this->displayName = $this->l('Homepage Banner');
$this->description = $this->l('This is a module that allows to show a banner in the homepage of the website.');
$this->confirmUninstall = $this->l('Are you sure you want to uninstall?');
}
}
Per permettere di installare (e disinstallare) il modulo correttamente, è necessario aggiungere due ulteriori metodi alla nostra classe. Si tratta dei metodi install() e uninstall().
Osserviamo, nel codice seguente, che il metodo install(), oltre a invocare la funzione padre di installazione, effettua una serie di operazioni, ognuna delle quali deve restituire true. È importante notare che tra queste, viene chiamata una funzione che lega il modulo ad un’entità denominata hook. Un hook, come vedremo successivamente, è un modo per associare una parte di codice (solitamente per iniettare del contenuto in una pagina, ma possono essere usati anche per effettuare azioni più complesse) a determinati eventi di PrestaShop.
Nel nostro caso usiamo l’hook displayHome che ci permette di inserire contenuto nella homepage del sito.
Infine impostiamo un messaggio di default per il nostro banner.
Al contrario, il metodo per la disinstallazione del nostro modulo invoca il metodo padre per la disinstallazione e elimina il valore dalla tabella delle proprietà.
public function install() { if (Shop::isFeatureActive()) Shop::setContext(Shop::CONTEXT_ALL); return parent::install() && $this->registerHook('displayHome') && Configuration::updateValue('homepage_banner_message', 'Message shown in homepage'); } public function uninstall() { if (!parent::uninstall() || !Configuration::deleteByName('homepage_banner_message')) return false; return true; }
Cosa fare del nostro povero hook che abbiamo registrato con tanto amore?
Innanzitutto, prepariamo il template del banner che vogliamo inserire nella homepage del nostro e-commerce. Per farlo dobbiamo rispettare una particolare gerarchia di directory. Il file che andiamo a creare, e che chiamiamo homepage_banner.tpl, andrà inserito all’interno della directory /views/templates/hook/.
Notate l’uso della variabile {$homepage_banner_message} all’interno del file: grazie a Smarty, il template engine PHP utilizzato da PrestaShop, sarà possibile passare il testo da inserire nel banner.
<div style="text-align:center; font-size: 20px; font-weight: bold; background-color: black; color: white; padding: 20px; margin: 40px 0;"> <span>{$homepage_banner_message}</span> </div>
Adesso che abbiamo il nostro template, possiamo definire la funzione che ci permetterà di usare l’hook per iniettare il suo contenuto nell’homepage del sito e-commerce. È importante notare l’uso delle maiuscole nel definire la funzione: l’hook da noi usato è displayHome, di conseguenza la funzione dovrà chiamarsi necessariamente hookDisplayHome.
La funzione sostanzialmente utilizza le funzionalità di Smarty per iniettare la variabile relativa al messaggio da inserire nel banner prelevandola dalla tabella delle proprietà e chiama la funzione che mostrerà il banner.
public function hookDisplayHome($params) { $this->context->smarty->assign( array('homepage_banner_message' => Configuration::get('homepage_banner_message')) ); return $this->display(__FILE__, 'homepage_banner.tpl'); }
Per terminare lo sviluppo del modulo dobbiamo fornire la possibilità di modificare il messaggio mostrato all’interno del banner.
Per questo ci serviamo del metodo displayForm() per mostrare il form di configurazione e del metodo getContent() che permette di salvare il messaggio. Il primo metodo fa uso della classe HelperForm per generare un form con il campo messaggio e prelevandone il valore corrente da Configuration, oltre ad impostare altre caratteristiche le cui specifiche sono consultabili nella documentazione ufficiale di PrestaShop.
public function displayForm() { // Definiamo il titolo della sezione, i campi del form e il tasto di salvataggio $fields_form[0]['form'] = array( 'legend' => array( 'title' => $this->l('Homepage Banner'), ), 'input' => array( array( 'type' => 'text', 'label' => $this->l('Banner message'), 'name' => 'homepage_banner_message', 'lang' => true, 'size' => 20, 'required' => true ), ), 'submit' => array( 'title' => $this->l('Save'), 'class' => 'btn btn-default pull-right' ) ); // Istanziamo l'HelperForm che permette di costruire la schermata di configurazione del modulo $helper = new HelperForm(); $helper->module = $this; $helper->name_controller = $this->name; $helper->token = Tools::getAdminTokenLite('AdminModules'); $helper->currentIndex = AdminController::$currentIndex.'&configure='.$this->name; $helper->title = $this->displayName; $helper->show_toolbar = true; $helper->toolbar_scroll = true; $helper->submit_action = 'submit'.$this->name; $helper->toolbar_btn = array( 'save' => array( 'desc' => $this->l('Save'), 'href' => AdminController::$currentIndex.'&configure='.$this->name.'&save'.$this->name. '&token='.Tools::getAdminTokenLite('AdminModules'), ), 'back' => array( 'href' => AdminController::$currentIndex.'&token='.Tools::getAdminTokenLite('AdminModules'), 'desc' => $this->l('Back to list') ) ); // Preleva il valore corrente del messaggio per popolare il form $helper->fields_value['homepage_banner_message'] = Configuration::get('homepage_banner_message'); return $helper->generateForm($fields_form); }
Il metodo getContent(), infine, permette il salvataggio del testo del messaggio utilizzando il form definito mediante la funzione displayForm() avendo cura di validare il valore sottomesso e, passata la validazione, aggiornando il valore in Configuration.
public function getContent() { $response = null; // Se il form è stato sottomesso controlliamo il valore del campo del form if (Tools::isSubmit('submit'.$this->name)) { $homepageBannerMessage = strval(Tools::getValue('homepage_banner_message')); // Se il campo del messaggio è vuoto allora restituiamo un errore if (!isset($homepageBannerMessage)) $response .= $this->displayError($this->l('Banner message can't be empty.')); else { // Se invece il campo non è vuoto aggiorniamo il valore in Configuration Configuration::updateValue('homepage_banner_message', $homepageBannerMessage); $response .= $this->displayConfirmation($this->l('Banner message updated.')); } } return $response.$this->displayForm(); }
A questo punto il nostro modulo è completo. Possiamo quindi andare all’interno della sezione Moduli ed installarlo.

Cliccando sul tasto Configura è possibile visualizzare il form da noi creato per andare a modificare il messaggio del banner.

Et voilà! Il nostro banner in homepage. Noterete che si trova alla fine della pagina. È possibile cambiare l’ordine di visualizzazione andando a modificarlo all’interno della sezione Design -> Posizioni.

Come potrete capire da questa guida, creare un modulo per PrestaShop prevede la conoscenza della sua struttura e dei meccanismi che regolano il funzionamento degli hook. Grazie ad essi è possibile aggiungere nuove funzionalità e blocchi all’interno delle varie pagine del sito e-commerce. È anche possibile registrare nuovi hook personalizzati (avendo cura di aggiungerli nel template del tema in cui vogliamo farlo intervenire) o utilizzare particolari hook per inserire codice JavaScript o aggiungere un foglio di stile CSS. Le possibilità sono infinite, serve solo fantasia, oltre ovviamente a consultare la documentazione ufficiale di PrestaShop.
The post E-commerce: guida alla creazione di un modulo PrestaShop first appeared on Oimmei Digital Consulting.
]]>The post Sviluppo App in SwiftUI first appeared on Oimmei Digital Consulting.
]]>SwiftUI nasce per la costruzione di interfacce utente in maniera semplice, veloce e soprattuto uniforme tra tutte le diverse piattaforme Apple. Lo stesso componente infatti verrà renderizzato e si comporterà automaticamente nella maniera più appropriata a seconda del dispositivo dove viene utilizzato. Questo permetterà a chi lavora nello sviluppo app di preoccuparsi meno dei diversi aspetti di ciascuna piattaforma (ovviamente alcune distinzioni andranno sempre fatte, ci sono cose che si possono fare su MacOS mentre su WatchOS no ad esempio) e focalizzarsi maggiormente sulle funzionalità delle proprie applicazioni.
SwiftUI si basa su due principi fondamentali: una sintassi dichiarativa e un nuovo sistema per l’aggiornamento delle informazioni che dovranno aggiornare l’interfaccia.
Differentemente da quanto accade normalmente in UIKit, invece di raccogliere gli eventi e decidere come modificare l’interfaccia utente con una sintassi dichiarativa, è necessario descrivere a priori che cosa dovrà fare l’interfaccia e come dovrà comportarsi. Al variare dei dati l’interfaccia semplicemente si aggiornerà per riflettere tali cambiamenti. Come scritto in uno dei primi esempi forniti da Apple, sarà possibile scrivere che volete visualizzare una lista di elementi formata da una label per il titolo e una per il sottotitolo, descrivere la font, l’allineamento e il colore per ciascun componente.
Queste informazioni rappresentano la descrizione di ciascuna View, che verrà renderizzata e rinfrescata automaticamente da SwiftUI al variare delle condizioni di stato definite. Lo stato infatti è il cuore centrale di una View, quello che Apple definisce Source of Truth (fonte della verità), ciascuna View deve avere almeno un oggetto che funga da Source of Truth e che ne scateni gli aggiornamenti automatici (a meno di non voler disegnare una view completamente statica che è una casistica plausibile). Ad esempio uno Switch (chiamato Toggle in SwitftUI) varierà automaticamente il suo stato “grafico” da On a Off al variare di una variabile booleana a cui è agganciato e viceversa, quindi modificare a codice la stato della variabile porterà SwiftUI a ridisegnare la view per aggiornare lo switch mentre l’utente che manualmente effettua un tap sul componente aggiornerà il valore della variabile a cui è agganciato.
Questo ci porta al secondo principio su cui si basa SwiftUI, Combine un nuovo framework, introdotto sempre durante il WWDC del 2019, che come SwiftUI si prefigge di descrivere in modo dichiarativo come i valori vengano modificati nel corso del tempo. Scopo di questo articolo non è approfondire i concetti dietro Combine, che può essere usato anche con il buon vecchio UIKit, anche perché fortunatamente Apple ha già reso compatibile con Combine numerosi framework di base.
Dopo questa lunga introduzione credo che il modo migliore per capire come funzioni SwiftUI sia lavorare insieme allo sviluppo app di una semplice applicazione e provare a capire come si costruiscono e si possa interagire con le view. Ed essendo programmatori ho pensato di lavorare sullo sviluppo app che ci permetta di visualizzare e scegliere il nostro carburante preferito, il caffè. Essendo io un amante del caffè di Starbucks ho pensato di raccogliere le informazioni presenti su alcuni dei loro prodotti, presenti sul sito ufficiale, e creare una piccola App che mi permetta di visualizzare i caffè e scegliere i miei preferiti. Per l’occasione ho preparato una lista in json di alcuni esempi di caffè e le relative immagini da associare.
Scaricate il file zip con gli asset e siamo pronti a partire: DevelopersFuel Materiale
Iniziate aprendo Xcode, create un nuovo progetto e selezionate Single View App.
Nella schermata successiva come product name scrivete DeveloperFuel facendo attenzione a scegliere SwiftUI come User Interface. Rimuovete dalle spunte Use core data, Include Unit Tests e Include UI Tests.
Se è la prima volta che provate ad usare un template per SwiftUI noterete che l’interfaccia è notevolmente diversa dai progetti UIKit a cui siete abituati. Infatti la parte sinistra di Xcode è riempita da una vasta porzione che mostra in tempo reale un preview della vostra applicazione, e quando dico in tempo reale intendo che man mano che scriverete il codice l’interfaccia del preview si aggiornerà e, viceversa, aggiungendo componenti al preview il codice viene aggiornato di conseguenza.
A questo punto aprite lo zip con il materiale che avete precedentemente scaricato, troverete una cartella Images che contiene le immagini da visualizzare dei nostri preziosi caffè. Trascinate tutte le immagini all’interno dell’Asset Catalog di default.
All’interno della cartella Model troverete 3 files: BundleJsonHelper.swift, Coffee.swift e coffees.json, aggiungete anche questi tre files al progetto Xcode e siamo pronti per iniziare. Si tratta di un file json che contiene il nostro modello dati iniziale, la definizione della classe Coffee e un helper per semplificare il caricamento del file json all’interno del modello dati.
Aprite il file Coffee.swift e date un occhiata a come sarà strutturato il nostro modello: ci sono 2 oggetti CoffeeType e Coffee, il primo raggrupperà un certo numero di caffè. Per ogni caffè sono definite alcune proprietà come nome, descrizione, foto e ingredienti. Prestate particolarmente attenzione alle ultime righe del file, viene creata un istanza statica chiamata CoffeeExample di un oggetto Coffee, questo risulterà fondamentale durante la costruzione delle nostre view.
SwiftUI nasce intorno all’idea della composizione e riusabilità delle singole View, invece di descrivere un’interfaccia complessa nella sua interezza è preferibile (e sopratutto molto più leggibile) scomporla in tanti piccoli blocchi atomici da comporre a nostro piacimento ed eventualmente riutilizzare in altre parti dell’App. La nostra App dovrà per prima cosa presentare una lista di diversi tipi di caffè (altrimenti cosa mi sono messo a copiare in file json tutte quelle informazioni dal sito di Starbucks?!?), e per fare questo per prima cosa abbiamo bisogno di rappresentare un singolo elemento di questa lista, che per semplicità chiameremo CoffeeRow.
Create un nuovo file da aggiungere al progetto, e dalla sezione User Interface selezionate SwiftUI View. Chiamate il file CoffeeRow e proseguite:
La prima di cui abbiamo bisogno è una proprietà che contenga i dati da visualizzare, aggiungete quindi subito dopo la definizione della struct:
var coffee: Coffee
Immediatamente il compilatore si arrabbierà indicando che manca il parametro coffee all’interno del preview. Del preview? Si esatto, perchè i preview in tempo reale che vedete sulla sx non sono altro che strutture particolari di SwiftUI che vengono compilate e renderizzate mentre modificate il codice. In alcuni casi, quando le modifiche sono troppo sostanziose per essere calcolate durante la scrittura del codice, il preview viene messo in pausa automaticamente, una volta terminate le modifiche è possibile farlo ripartire premendo il pulsante resume in alto a sinistra oppure option+command+p sulla tastiera.
Risolviamo subito il problema modificando la riga di errore da CoffeeRow() in CoffeeRow(coffee: Coffee.coffeExample). Questa è l’istanza statica che avete notato nel modello dati precedentemente. Non appena modificata la dichiarazione l’errore scomparirà e il preview si aggiornerà mostrando il confortante Hello World!, se questo non dovesse succedere avviate a mano il resume del preview.
Una view con il testo hello world però non è la più utile delle view, iniziamo quindi a mostrare alcuni dettagli che possono essere utili ai nostri utenti (o noi stessi) quando visualizziamo una lista di caffè tra cui scegliere.
Il componente principale di ciascuna View in SwiftUI è la variabile body, che deve necessariamente ritornare un oggetto di tipo View. Questa è quella che rappresenterà la nostra interfaccia utente a runtime. É importante notare che deve ritornare un singolo oggetto View, quindi non è possibile ad esempio inserire un secondo oggetto Text() sotto a quello presente, perchè appunto sarebbero 2 view.
Ricordate il concetto di composizione di cui abbiamo parlato prima? Gli oggetti possono essere però contenuti in altri oggetti, e per questo abbiamo dei contenitori che ci permettono di raggruppare i nostri elementi di interfaccia… gli Stack!
Iniziamo creando uno stack verticale, racchiudendo il nostro testo in esso:
VStack {
Text(“Hello World”)
}
Non un grande cambiamento per adesso ma questo è solo il primo passo. Adesso è giunto il momento di visualizzare qualcosa di vero, sostituiamo quindi il contenuto di Text con Text(coffee.name). Immediatamente nel preview la scritta Hello World! verrà sostituita con Caffè Mocha, proprio il nome del caffe che abbiamo inizializzato durante la creazione del nostro preview.
Si inizia già ad intravedere le potenzialità di SwiftUI ma ancora la nostra interfaccia non è ne carina ne esaustiva. Proviamo ad aggiungere la descrizione sotto il nome, aggiungiamo quindi un’altra Text sotto quella già presente e stavolta mostriamo la descrizione.
var body: some View {
VStack {
Text(coffee.name)
Text(coffee.description)
}
}
A questo punto la nostra preview dovrebbe essersi aggiornata con il titolo e la descrizione del caffe, perfettamente centrate una sotto l’altra… centrate? Avete mai visto una lista con titolo e sotto titolo centrate? Questo perchè di default un VStack ha un allineamento centrato, ma ovviamente è possibile modificare questa impostazione molto semplicemente.
Per farlo vi mostrerò un’altro modo di interazione con l’interfaccia di Xcode, tenendo premuto option cliccate su VStack nell’editor e dal menu contestuale selezionate Show SwiftUI Inspector:
Si aprirà l’inspector con le proprietà per lo stack verticale, un ottimo modo per scoprire che cosa sia possibile personalizzare e modificare per ciascun componente. Modificando le impostazioni di allineamento (alignment) sia il codice che il preview si aggiorneranno, mostrando i cambiamenti scelti.
Selezionate leading e chiudete. Adesso è molto meglio!
Iniziamo adesso ad applicare un po’ di stile alla nostra cella, in SwiftUI si fa aggiungendo dei modificatori (view modifiers appunto) a ciascuna view. Ogni tipologia di view ha i suoi modificatori particolari, più ovviamente quelli ereditati dalle classi padre, in particolare per le nostre textview sarà possibile modificare font, colore, numero di linee, etc…
Per prima cosa modifichiamo il nome del caffè perché risalti di più, cambiando la sua font da body (la standard) a headline:
Text(coffee.name)
.font(.headline)
E allo stesso tempo rendiamo meno importante la descrizione limitandone anche il numero di linee visualizzate:
Text(coffee.description)
.font(.subheadline)
.foregroundColor(.secondary)
.lineLimit(2)
I view modifiers possono essere concatenati tra di loro, ma attenzione perché l’ordine è importante. In questo caso non sarebbe cambiato niente, ma con altri modificatori si possono ottenere risultati molto diversi in base all’ordine con cui vengono applicati.
var body: some View {
VStack(alignment: .leading) {
Text(coffee.name)
.font(.headline)
Text(coffee.description)
.font(.subheadline)
.foregroundColor(.secondary)
.lineLimit(2)
}
}
Nel nostro preview iniziamo a vedere già una versione molto più accattivante di quella che potrebbe essere un elemento di una lista di caffè ma quello che manca è l’immediatezza del colpo d’occhio, è il momento giusto per inserire una bella immagine! Vogliamo la nostra immagine alla destra del titolo e della descrizione, ma i nostri componenti sono racchiusi in uno stack verticale che ci permette di impilare gli oggetti uno sopra l’altro… fortunatamente alla fine anche uno stack è una view, e può essere incapsulata in un’altra view. Inseriamo quindi lo stack verticale in uno stack orizzontale, che conterrà la nostra immagine e lo stack verticale con nome e descrizione:
HStack {
VStack(alignment: .leading) {
Text(coffee.name)
.font(.headline)
Text(coffee.description)
.font(.subheadline)
.foregroundColor(.secondary)
.lineLimit(2)
}
}
Prima del VStack aggiungiamo un immagine:
HStack {
Image(coffee.photo)
.resizable()
.frame(width: 60, height: 60)
…
E’ importante notare che abbiamo dovuto aggiungere il modificatore .resizable(), altrimenti la foto avrebbe occupato tutto lo spazio necessario per le sue dimensioni e solo dopo abbiamo potuto specificare le esatte dimensioni che occuperà.
Il risultato è già ottimo e in pochissime righe di codice, ma non sarebbe più professionale se le foto avessero una maschera circolare come in tutte le migliori applicazioni? Niente di più semplice, ogni View ha un modificatore per “clippare”, ovvero ritagliare l’oggetto in base ad una maschera lungo i bordi, aggiungiamo quindi:
Image(coffee.photo)
.resizable()
.frame(width: 60, height: 60)
.clipShape(Circle())
Finita! Il nostro elemento di lista è pronto per essere utilizzato!
import SwiftUI
struct CoffeeRow: View {
var coffee: Coffee
var body: some View {
HStack {
Image(coffee.photo)
.resizable()
.frame(width: 60, height: 60)
.clipShape(Circle())
VStack(alignment: .leading) {
Text(coffee.name)
.font(.headline)
Text(coffee.description)
.font(.subheadline)
.foregroundColor(.secondary)
.lineLimit(2)
}
}
}
}
struct CoffeeRow_Previews: PreviewProvider {
static var previews: some View {
CoffeeRow(coffee: Coffee.coffeeExample)
}
}
Aprite adesso di nuovo il file ContentView.swift, dove di nuovo il template aveva generato un meraviglioso Hello World, questo è il file che viene mostrato all’apertura dell’App, e qui è dove andremo ad inserire la nostra lista. Prima di visualizzare una lista è necessario avere degli oggetti da mostrare, occorre quindi caricare l’elenco dei caffè presenti nel file json che avevo preparato. Aggiungete prima del body
let menu = Bundle.main.decode([CoffeeType].self, from: "coffees.json")
Per semplicemente caricare il nostro modello dati con il contenuto del file json, questa volta non essendo una variabile ma una costante il compilatore non avrà di che lamentarsi e il preview continuerà a funzionare come prima. Adesso sostituite la text View con questo:
List {
ForEach(menu) { menuItem in
Section(header: Text(menuItem.name).font(.title)) {
ForEach(menuItem.coffees) { coffee in
CoffeeRow(coffee: coffee)
}
}
}
}
Come per magia nel preview apparirà una lista, divisa in sezioni, con ciascuna sezione riempita con i caffè che contiene. Vediamo come questo sia possibile: abbiamo aggiunto una List, l’equivalente di una UITableView, al suo interno abbiamo scorso tutti gli elementi del menu con il costruttore ForEach. Se vi ricordate il nostro modello aveva un certo numero di tipi di caffè e ciascun tipo conteneva i suoi caffè. Abbiamo chiamato ciascun tipo menuItem e lo abbiamo usato per costruire la sezione corrispondente.
La view Section ha come parametri opzionali header e footer, noi abbiamo deciso di utilizzare l’header, che come al solito si aspetta una View (composizione, composizione, composizione): per semplicità abbiamo passato una Text con il nome del tipo di caffè modificandone la font perché sia più grande. Nella closure della sezione è necessario passare gli elementi che la compongono, di nuovo è necessario scorrere tutti gli elementi della sezione corrente e ritornare la view da visualizzare, in questo caso proprio l’oggetto CoffeeRow che abbiamo creato prima.
Sebbene sia già abbastanza manca ancora qualcosa, la barra del titolo! In UIKit questo si può ottenere utilizzando un NavigationController, in SwiftUI possiamo dire che il suo equivalente è la NavigationView. Racchiudete la List all’interno di una NavigationView in questo modo
NavigationView {
List {
ForEach(menu) { menuItem in
Section(header: Text(menuItem.name).font(.title)) {
ForEach(menuItem.coffees) { coffee in
CoffeeRow(coffee: coffee)
}
}
}
}
}
e nel preview comparirà immediatamente una Navigation Bar…. vuota! Ovviamente è perché non abbiamo dato nessun nome alla nostra view, qui le cose diventano un pochino più confusionarie. Per quello che abbiamo visto fino ad ora ci si aspetterebbe di aggiungere un modificatore alla NavigationView, impostando i parametri che vogliamo (titolo, pulsanti, etc…). Ricordate però che una NavigationView è l’equivalente di un NavigationController, ogni successiva view che verrà aggiunta dovrà avere il suo titolo e i suoi pulsanti. Aggiungere dei modificatori direttamente alla NavigationView significherebbe applicarli a tutte le successive view che conterrà durante il suo life cycle, per questo è necessario applicare i modificatori direttamente agli elementi che contiene.
Quindi direttamente alla List aggiungete
List {
ForEach(menu) { menuItem in
Section(header: Text(menuItem.name).font(.title)) {
ForEach(menuItem.coffees) { coffee in
CoffeeRow(coffee: coffee)
}
}
}
}
.listStyle(GroupedListStyle())
.navigationBarTitle(Text("Choose your fuel"))
e anche il titolo di questa View verrà correttamente visualizzato. Già che c’eravamo ho aggiunto lo stile Grouped per un effetto visivo migliore.
E’ giunto il momento di verificare il lavoro fatto (il Preview in tempo reale è bellissimo, ma vedere girare l’App è tutta un’altra cosa). Selezionate un simulatore dalla lista e lanciate l’App. Adesso abbiamo una meravigliosa lista di caffè, realizzata in pochissimo tempo, che però è completamente inutile! ?
Non sarebbe meglio se una volta scelto il nostro caffè, potessimo vedere nel dettaglio la sua descrizione e magari avere un lista degli ingredienti principali? Iniziamo quindi a disegnare la nostra View che ospiterà il dettaglio del caffè: per prima cosa mostreremo la foto più grande, poi il nome, la descrizione e l’elenco degli ingredienti.
Create un nuovo file SwiftUI e chiamatelo CoffeeDetail. Per visualizzare i dati di un caffè occorre averlo, create quindi una variabile chiamata coffee che verrà popolata a runtime con quello selezionato dalla lista.
import SwiftUI
struct CoffeeDetail: View {
var coffee: Coffee
var body: some View {
Text("Hello, World!")
}
}
struct CoffeeDetail_Previews: PreviewProvider {
static var previews: some View {
CoffeeDetail(coffee: Coffee.coffeeExample)
}
}
Abbiamo detto di voler visualizzare prima la foto e sotto il resto delle informazioni. Avremo bisogno quindi di un Vertical Stack con all’interno la nostra foto. Sostituite a Text(“Hello World”) :
VStack {
Image(coffee.photo)
.resizable()
.clipShape(Circle())
}
Nel preview comparirà l’immagine centrata, ritagliata fino ad occupare tutto lo spazio in orizzontale del device. Forse un pochino troppo, provate ad inserire in po di padding orizzontale in questo modo:
VStack {
….
….
}
.padding(.horizontal)
meglio… ma c’è qualcosa che ancora non va… il caffè non è molto invitante, forse perché l’immagine ha un aspect ratio sbagliato. Aggiungete sotto il modificatore resizable() .aspectRatio(contentMode: .fit) e immediatamente il nostro caffè diventerà molto più invitante.
var body: some View {
VStack {
Image(coffee.photo)
.resizable()
.aspectRatio(contentMode: .fit)
.clipShape(Circle())
}
.padding(.horizontal)
}
Dovremmo però cercare di far risaltare un po’ di più la nostra immagine, magari aggiungendo un bordo e un po di ombreggiatura… niente di più semplice in SwiftUI! Magari potrebbe anche essere utile in futuro riutilizzare questo tipo di immagine da altre parti nell’App, quindi invece di modificare direttamente la nostra view ne creeremo una specializzata nel visualizzare le immagini.
Create un nuovo file SwiftUI e chiamatelo RoundImage, aggiungente una variabile per contenere il nome dell’immagine che dovrà essere visualizzato e aggiungente un componente Image che la carichi.
import SwiftUI
struct RoundImage: View {
var imageName: String
var body: some View {
Image(imageName)
}
}
struct RoundImage_Previews: PreviewProvider {
static var previews: some View {
RoundImage(imageName: "Americano")
}
}
Adesso aggiungiamo il clip circolare, il resize e l’aspectRatio come prima. Per ottenere un effetto migliore possiamo aggiungere un bordo, in SwiftUI semplicemente aggiungendo un overlay alla nostra immagine definendone lo shape e lo stroke:
.overlay(Circle().stroke(Color.gray, lineWidth: 4))
E per dare l’effetto “pop” magari aggiungiamo un’ombra:
.shadow(radius: 10)
Perché si veda l’ombra cambiate il colore dello stroke in bianco:
var body: some View {
Image(imageName)
.resizable()
.aspectRatio(contentMode: .fit)
.clipShape(Circle())
.overlay(
Circle().stroke(Color.white, lineWidth: 4))
.shadow(radius: 10)
}
Sostituiamo adesso all’immagine di CoffeeDetail la nostra nuova RoundImage in questo modo:
var body: some View {
VStack {
RoundImage(imageName: coffee.photo)
}
.padding(.horizontal)
}
e otterremo immediatamente un effetto più cool per la nostra View. Adesso non resta che aggiungere le altre informazioni necessarie, come la descrizione, l’elenco degli ingredienti e ovviamente il nome.
Per poter visualizzare il nome del caffè in stile iOS conviene metterlo nel titolo della Navigation Bar, che come detto in precedenza è un modificatore della View stessa. Aggiungente quindi ai modificatori del VStack :
.navigationBarTitle(coffee.name)
Per vedere il risultato direttamente nel preview dovete ricordarvi di incapsulare la view renderizzata per il preview in un proprio NavigationView, questo perché è un elemento statico slegato dal flusso dell’App, ovviamente nel lifecycle normale dell’App questa sarà all’interno del navigation view che avete impostato nella Lista.
struct CoffeeDetail_Previews: PreviewProvider {
static var previews: some View {
NavigationView {
CoffeeDetail(coffee: Coffee.coffeeExample)
}
}
}
Sotto l’immagine possiamo andare ad inserire la descrizione ed un divisorio per dare più aria alla sezione che conterrà gli ingredienti:
var body: some View {
VStack(alignment: .leading) {
RoundImage(imageName: coffee.photo)
Text(coffee.description)
.font(.body)
Divider()
}
.padding(.all)
.navigationBarTitle(coffee.name)
}
Modificate anche l’allineamento dello Stack in .leading, in questo modo gli oggetti non saranno centrati nella view ma allineati a sinistra.
Gli ingredienti sono costituiti nel nostro modello dati da un array di stringhe, per migliorare un po la loro visualizzazione potremmo trasformarli in un elenco puntato, creiamoci un componente apposito per questo. Create un nuovo file SwiftUI e chiamatelo IngredientsList, aggiungete una variabile ingredients che conterrà il nostro array di stringhe come di seguito:
struct IngredientsList: View {
var ingredients: [String]
var body: some View {
…
}
}
Per visualizzare un elenco occorre uno Stack verticale, un identificatore e il testo… iniziamo inserendo al posto del placeholder Text un VStack, specificandone l’allineamento .leading, all’interno del quale dobbiamo renderizzare una riga per ciascun ingrediente. Come per la lista dei caffè possiamo utilizzare il ForEach per scorrere tutti gli elementi dell’array di ingredienti, l’unica differenza è che stavolta occorre specificare un identificativo univoco per ciascun elemento. Mentre i nostri oggetti CoffeeType implementano il protocollo Identifiable ciò non è direttamente vero per le stringhe, quindi è necessario comunicare a SwiftUI che ciascuna stringa è univoca:
struct IngredientsList: View {
var ingredients: [String]
var body: some View {
VStack(alignment: .leading) {
ForEach(ingredients, id: .self) { ingredient in
….
}
}
}
}
Utilizzando il parametro id: e assegnandoli .self abbiamo risolto il nostro problema.
Ogni riga avrà poi un immagine e un testo, occorre quindi un HStack per allinearli correttamente:
struct IngredientsList: View {
var ingredients: [String]
var body: some View {
VStack(alignment: .leading) {
ForEach(ingredients, id: .self) { ingredient in
HStack {
Image(systemName: "largecircle.fill.circle")
Text(ingredient)
.fontWeight(.semibold)
}
.font(.subheadline)
}
}
}
}
Per l’immagine abbiamo usato una di quelle fornite all’interno dei simboli di sistema, che hanno il vantaggio di essere trattate come vere e proprie font, per questo abbiamo potuto applicare il modificatore .font direttamente a tutto l’HStack, di fatto applicandolo a tutti gli elementi che contiene.
Terminato il nostro componente possiamo utilizzarlo nel dettaglio, aprite di nuovo il file CoffeeDetail e aggiungete:
struct CoffeeDetail: View {
var coffee: Coffee
var body: some View {
VStack(alignment: .leading) {
RoundImage(imageName: coffee.photo)
Text(coffee.description)
.font(.body)
Divider()
Text("Ingredients")
.font(.headline)
IngredientsList(ingredients: coffee.ingredients)
}
.padding(.all)
.navigationBarTitle(coffee.name)
}
}
Manca solamente un piccolo dettaglio per completare la View, non tutti i dispositivi hanno la stesse dimensioni, sopratutto in altezza. Contenuti come questi possono variare di molto anche in base alla mole dei dati che visualizzano, è necessario quindi che vi sia la possibilità di scrollare la view quando i dati non riescono ad essere rappresentati per intero. Notoriamente utilizzare una ScrollView su UIKit è sempre stato un incubo, fortunatamente in SwiftUI è una manna dal cielo, l’unica cosa da fare è racchiudere il VStack in una ScrollView, senza preoccuparsi di altro.
struct CoffeeDetail: View {
var coffee: Coffee
var body: some View {
ScrollView {
VStack(alignment: .leading) {
RoundImage(imageName: coffee.photo)
Text(coffee.description)
.font(.body)
Divider()
Text("Ingredients")
.font(.headline)
IngredientsList(ingredients: coffee.ingredients)
}
.padding(.all)
.navigationBarTitle(coffee.name)
}
}
}
Adesso non manca che visualizzare il dettaglio corretto quando viene selezionato un elemento dalla lista, il NavigationLink è l’elemento preposto per questo scopo, a noi occorre un NavigationLink specifico per ciascun elemento della lista. In ContentRow sostituite la riga contente CoffeeRow con:
NavigationLink(destination: CoffeeDetail(coffee: coffee)) {
CoffeeRow(coffee: coffee)
}
Immediatamente nel preview comparirà il classico disclosure indicator per ciascuna cella lista.
Provata a far partire l’App sul simulatore e adesso potete navigare avanti e indietro tra tutti i caffè della lista.
In breve tempo siamo riusciti a lavorare allo sviluppo App, seppur semplice, ma che ha la struttura classica della maggior parte delle App per iOS. Spero di avervi fatto percepire le enormi potenzialità di SwiftUI, che pur essendo agli inizi già è in grado di velocizzare notevolmente la creazione delle interfacce. Ancora non è possibile utilizzare SwiftUI per lo sviluppo app di qualsiasi tipo di applicazione, ci sono ancora molti casi in cui è più semplice utilizzare UIKit per lo stesso task, ma già dalla versione 2.0 presentata quest’anno al WWDC sono stati introdotte notevoli migliorie e nuovi componenti, indicando che la strada tracciata è la stessa intrapresa diversi anni fa con Swift.
Ovviamente ci sono ancora moltissimi aspetti di SwiftUI che andrebbero affrontati, ma questa voleva semplice essere una introduzione per eventualmente stimolare la vostra curiosità ad andare più a fondo, perché se non sarà tra un anno o due, la sensazione è che “questa è la via”. 
Noi in Oimmei siamo stati abbastanza “folli” da esserci occupati dello sviluppo app in SwiftUI per una delle ultime applicazioni che ci hanno richiesto, Kil0, sinceramente non è stato semplice, ma abbiamo imparata veramente tanto. Se me la sentirei di consigliare adesso lo sviluppo App in SwiftUI per un progetto completo e “complesso” come Kil0? Sinceramente io fossi in voi aspetterei di poter supportare da iOS 14 in su… ma iniziate subito a prendere familiarità con la tecnologia, create un widget, utilizzate SwiftUI per alcune view o parti della vostra app in UIKit, e sicuramente ne sarete ripagati.
The post Sviluppo App in SwiftUI first appeared on Oimmei Digital Consulting.
]]>The post Satis e Composer: come cambiano la vita di uno sviluppatore first appeared on Oimmei Digital Consulting.
]]>The post Satis e Composer: come cambiano la vita di uno sviluppatore first appeared on Oimmei Digital Consulting.
]]>The post Eseguire task dopo una chiamata HTTP con Symfony first appeared on Oimmei Digital Consulting.
]]>The post Eseguire task dopo una chiamata HTTP con Symfony first appeared on Oimmei Digital Consulting.
]]>The post La richiesta dei permessi di Android first appeared on Oimmei Digital Consulting.
]]>fun checkPermissions() {
// Controlliamo se i permessi sono stati concessi...
if (ContextCompat.checkSelfPermission(thisActivity,
Manifest.permission.ACCESS_FINE_LOCATION)
!= PackageManager.PERMISSION_GRANTED) {
// Permessi non concessi
// Dobbiamo mostrare una spiegazione?
if (ActivityCompat.shouldShowRequestPermissionRationale(thisActivity,
Manifest.permission.ACCESS_FINE_LOCATION)) {
// Mostra una spiegazione del perchè la mancanza di questi permessi
// può negare alcune funzionalità. Questa spiegazione può essere
// data con un semplice AlertDialog(). Alla riposta positiva (l'utente
// accetta di dare i permessi) andremo a richiedere i permessi con
// le istruzioni predefiniti (es. ActivityCompat.requestPermissions([...])
// come mostrato qui sotto
} else {
// Nessuna spiegazione da dare, richiediamo direttamente i permessi
ActivityCompat.requestPermissions(thisActivity,
arrayOf(Manifest.permission.ACCESS_FINE_LOCATION),
REQUEST_LOCATION_PERMISSIONS)
//REQUEST_LOCATION_PERMISSIONS è una costante che andremo ad utilizzare
// nel metodo onRequestPermissionsResults([...]) per analizzare i risultati
// ed agire di conseguenza
}
} else {
// Fantastico, abbiamo già i permessi, possiamo fare tutti i danni che vogliamo :D
}
}
Il flusso che vi ho descritto qua sopra può essere considerato abbastanza “standard”, ovvero può adattarsi a qualsiasi tipo di permesso. Il concetto, per concludere, è piuttosto semplice, quindi: Ho i permessi? Si: faccio le mie cose. No? Guardo se devo richiederli di nuovo, altrimenti mi rassegno ?
Ultimo e non ultimo, il metodo …
override fun onRequestPermissionsResults([...])
… in cui andremo ad esaminare se i permessi sono stati concessi ed eventualmente attivare / disattivare / far partire alcune funzionalità
override fun onRequestPermissionsResult(
requestCode: Int,
permissions: Array<out String>,
grantResults: IntArray
) {
super.onRequestPermissionsResult(requestCode, permissions, grantResults)
when (requestCode) {
REQUEST_LOCATION_PERMISSIONS -> {
map?.uiSettings.isMyLocationButtonEnabled = false
if (grantResults[0] == PackageManager.PERMISSION_GRANTED) {
button_position_on_me.visibility = View.VISIBLE
map.isMyLocationEnabled = true
} else {
button_position_on_me.visibility = View.GONE
map.isMyLocationEnabled = false
Snackbar.make(
view!!,
getString(R.string.gps_permissions_not_granted),
Snackbar.LENGTH_INDEFINITE
)
.setAction(android.R.string.ok) {
checkLocationPermissions()
}
.show()
}
}
}
}
Quindi, ricapitolando, ecco alcuni punti che potrebbero chiarirvi dei dubbi:
Alla prossima, gente! PS: dimenticavo, se i permessi vengono richiesti in un Fragment piuttosto che in un’Activity, ricordatevi di implementare il metodo onRequestPermissionsResults([…]) e di richiamare il super.onRequestPermissionsResults() nell’Activity, altrimenti potrete attendere la chiamata al onRequestPermissionsResults([..]) del Fragment fino alla notte dei tempi a venire.
The post La richiesta dei permessi di Android first appeared on Oimmei Digital Consulting.
]]>The post I nostri settori di competenza first appeared on Oimmei Digital Consulting.
]]>The post I nostri settori di competenza first appeared on Oimmei Digital Consulting.
]]>The post SAP Cloud Platform SDK per iOS first appeared on Oimmei Digital Consulting.
]]>Per la prima volta, tutte le aziende che utilizzano già SAP potranno avere dei client all’avanguardia con i quali controllare i propri dati aziendali ed interagire con i server SAP, unendo la potenza di calcolo dei backend SAP con le tecnologie all’avanguardia dei dispositivi Apple come iPhone, iPad ed Apple Watch.
In più SAP ha creato Fiori for iOS, un’estensione delle Human Interface Guidelines di Apple specificatamente per venire incontro alle esigenze di App gestionali, fornendo anche un serie di nuovi componenti UIKit adatti allo scopo.
Solo in Italia sono circa 4000 le PMI che nell’ultimo anno hanno utilizzato soluzioni SAP per la gestione dei propri processi aziendali (fonte digital4biz: ), aziende che sicuramente potranno beneficiare della libertà di azione fornita dai dispositivi Apple per gestire il proprio business in qualsiasi momento.
Noi di Oimmei siamo già all’opera per studiare il nuovo SDK proposto in modo da poter offrire a tutti i nostri clienti soluzioni native e sopratutto all’avanguardia per accedere ai propri sistemi SAP.
[photo credit: https://appdevelopermagazine.com]
The post SAP Cloud Platform SDK per iOS first appeared on Oimmei Digital Consulting.
]]>