La programmazione asincrona è un paradigma di programmazione che consente di scrivere codice che viene eseguito in modo asincrono
. A differenza della programmazione sincrona, che esegue il codice in sequenza, la programmazione asincrona consente al codice di essere eseguito in background mentre il resto del programma continua a funzionare. Questo è particolarmente utile per compiti che possono richiedere molto tempo per essere completati, come il recupero di dati da un’API remota.
La programmazione asincrona
è essenziale per creare applicazioni reattive ed efficienti in JavaScript. TypeScript, un superset di JavaScript, rende ancora più facile lavorare con la programmazione asincrona.
Ci sono diversi approcci alla programmazione asincrona
in TypeScript, tra cui l’uso di promesse
, async/await
e callback
. Tratteremo ciascuno di questi approcci in dettaglio in modo da poter scegliere il/i migliore/i per il tuo caso d’uso.
Indice dei contenuti
Perché è Importante la Programmazione Async?
La programmazione asincrona è cruciale per costruire applicazioni web reattive ed efficienti. Permette che i compiti vengano eseguiti in background mentre il resto del programma continua, mantenendo l’interfaccia utente reattiva agli input. Inoltre, la programmazione asincrona può migliorare le prestazioni complessive consentendo l’esecuzione simultanea di più compiti.
Ci sono molti esempi nel mondo reale di programmazione asincrona, come l’accesso alle fotocamere e ai microfoni degli utenti e la gestione degli eventi di input degli utenti. Anche se non crei frequentemente funzioni asincrone, è importante sapere come usarle correttamente per garantire che la tua applicazione sia affidabile e performante.
Come TypeScript Rende la Programmazione Async più Facile
TypeScript offre diverse funzionalità che semplificano la programmazione asincrona, tra cui sicurezza dei tipi
, inferenza dei tipi
, verifica dei tipi
e annotazioni di tipo
.
Con la sicurezza dei tipi, puoi garantire che il tuo codice si comporti come previsto, anche quando lavori con funzioni asincrone. Ad esempio, TypeScript può rilevare errori relativi a valori null e undefined durante il tempo di compilazione, risparmiandoti tempo e fatica nel debug.
L’inferenza e la verifica dei tipi di TypeScript riducono anche la quantità di codice boilerplate che devi scrivere, rendendo il tuo codice più conciso e facile da leggere.
E le annotazioni di tipo di TypeScript forniscono chiarezza e documentazione per il tuo codice, il che è particolarmente utile quando si lavora con funzioni asincrone che possono essere complesse da comprendere.
Ora immergiamoci e impariamo queste tre caratteristiche chiave della programmazione asincrona: promesse, async/await e callback.
Come utilizzare le promesse in TypeScript
Le promesse sono uno strumento potente per gestire operazioni asincrone in TypeScript. Ad esempio, potresti usare una promessa per recuperare dati da un’API esterna o per eseguire un’operazione che richiede tempo in background mentre il tuo thread principale continua a funzionare.
Per utilizzare una promessa, crei una nuova istanza della classe Promise
e le passi una funzione che esegue l’operazione asincrona. Questa funzione dovrebbe chiamare il metodo resolve con il risultato quando l’operazione ha successo o il metodo reject con un errore se fallisce.
Una volta che la promessa è stata creata, puoi allegare callback utilizzando il metodo then
. Queste callback verranno attivate quando la promessa viene soddisfatta, con il valore risolto passato come parametro. Se la promessa viene rifiutata, puoi allegare un gestore di errori utilizzando il metodo catch, che verrà chiamato con il motivo del rifiuto.
Utilizzare le promesse offre diversi vantaggi rispetto ai metodi tradizionali basati su callback. Ad esempio, le promesse possono aiutare a prevenire il “callback hell”, un problema comune nel codice asincrono in cui le callback annidate diventano difficili da leggere e mantenere.
Le promesse semplificano anche la gestione degli errori nel codice asincrono, poiché puoi utilizzare il metodo catch per gestire gli errori che si verificano ovunque nella catena delle promesse.
Infine, le promesse possono semplificare il tuo codice fornendo un modo coerente e componibile per gestire operazioni asincrone, indipendentemente dalla loro implementazione sottostante.
Come creare una Promise
Sintassi della Promise:
const myPromise = new Promise((resolve, reject) => {
// Esegui un'operazione asincrona
// Se l'operazione ha successo, chiama resolve con il risultato
// Se l'operazione fallisce, chiama reject con un oggetto di errore
});
myPromise
.then((result) => {
// Gestisci il risultato di successo
})
.catch((error) => {
// Gestisci l'errore
});
// Esempio 1 su come creare una Promise
function myAsyncFunction(): Promise<string> {
return new Promise<string>((resolve, reject) => {
// Alcune operazioni asincrone
setTimeout(() => {
// L'operazione di successo risolve la Promise. Dai un'occhiata al mio ultimo post sul blog su come padroneggiare la programmazione asincrona in TypeScript! Scopri come lavorare con Promises, Async/Await e Callbacks per scrivere codice efficiente e scalabile. Preparati a portare le tue competenze di TypeScript al livello successivo!
const success = true;
if (success) {
// Risolvi la Promise con il risultato dell'operazione se l'operazione è riuscita
resolve(
`The result is success and your operation result is ${operationResult}`
);
} else {
const rejectCode: number = 404;
const rejectMessage: string = `The result is failed and your operation result is ${rejectCode}`;
// Rifiuta la Promise con il risultato dell'operazione se l'operazione è fallita
reject(new Error(rejectMessage));
}
}, 2000);
});
}
// Usa la Promise
myAsyncFunction()
.then((result) => {
console.log(result); // output: Il risultato è successo e il risultato dell'operazione è 4
})
.catch((error) => {
console.error(error); // output: Il risultato è fallito e il risultato dell'operazione è 404
});
Nell’esempio sopra, abbiamo una funzione chiamata myAsyncFunction()
che restituisce una promise
. Utilizziamo il costruttore Promise
per creare la promise, che accetta una funzione di callback
con argomenti resolve
e reject
. Se l’operazione asincrona ha successo, chiamiamo la funzione di risoluzione. Se fallisce, chiamiamo la funzione di rifiuto.
L’oggetto promise restituito dal costruttore ha un metodo then()
, che accetta funzioni di callback per il successo e il fallimento. Se la promise si risolve con successo, la funzione di callback per il successo viene chiamata con il risultato. Se la promise è rifiutata, la funzione di callback per il fallimento viene chiamata con un messaggio di errore.
L’oggetto promise ha anche un metodo catch()
utilizzato per gestire gli errori che si verificano durante la catena di promise. Il metodo catch()
accetta una funzione di callback, che viene chiamata se si verifica un errore nella catena di promise.
Ora, passiamo a come concatenare le promise in TypeScript.
Come concatenare le promise
Concatenare le promise consente di eseguire più operazioni asincrone
in sequenza o in parallelo. Questo è utile quando è necessario eseguire diversi compiti asincroni uno dopo l’altro o contemporaneamente. Ad esempio, potrebbe essere necessario recuperare dati in modo asincrono e poi elaborarli in modo asincrono.
Vediamo un esempio di come concatenare le promise:
// Esempio su come funziona l'incatenamento delle promesse
// Prima promessa
const promise1 = new Promise((resolve, reject) => {
const functionOne: string = "This is the first promise function";
setTimeout(() => {
resolve(functionOne);
}, 1000);
});
// Seconda promessa
const promise2 = (data: number) => {
const functionTwo: string = "This is the second second promise function";
return new Promise((resolve, reject) => {
setTimeout(() => {
resolve(` ${data} '+' ${functionTwo} `);
}, 1000);
});
};
// Incatenamento della prima e della seconda promessa insieme
promise1
.then(promise2)
.then((result) => {
console.log(result); // output: Questa è la prima funzione di promessa + Questa è la seconda funzione di promessa
})
.catch((error) => {
console.error(error);
});
Nell’esempio sopra, abbiamo due promesse: promessa1
e promessa2
. promessa1
si risolve dopo 1 secondo con la stringa “Questa è la prima funzione di promessa”. promessa2
prende un numero in input e restituisce una promessa che si risolve dopo 1 secondo con una stringa che combina il numero in input e la stringa “Questa è la seconda funzione di promessa.”
Incateniamo le due promesse insieme usando il metodo then
. L’output promessa1
viene passato in input a promessa2
. Infine, usiamo nuovamente il metodo then
per registrare l’output di promessa2
sulla console. Se una qualsiasi delle promesse promessa1
o promessa2
viene rigettata, l’errore verrà catturato dal metodo catch
.
Congratulazioni! Hai imparato come creare e concatenare promesse in TypeScript. Ora puoi utilizzare le promesse per eseguire operazioni asincrone in TypeScript. Ora, esploriamo come funziona Async/Await
in TypeScript.
Come utilizzare Async / Await in TypeScript
Async/await è una sintassi introdotta in ES2017 per rendere più semplice il lavoro con le Promesse. Ti consente di scrivere codice asincrono che appare e si comporta come codice sincrono.
In TypeScript, puoi definire una funzione asincrona utilizzando la parola chiave async
. Questo indica al compilatore che la funzione è asincrona e restituirà una Promessa.
Ora, vediamo come utilizzare async/await in TypeScript.
Sintassi Async / Await:
// Sintassi Async / Await in TypeScript
async function functionName(): Promise<ReturnType> {
try {
const result = await promise;
// codice da eseguire dopo la risoluzione della promessa
return result;
} catch (error) {
// codice da eseguire se la promessa viene rifiutata
throw error;
}
}
Nell’esempio sopra, functionName
è una funzione asincrona che restituisce una Promessa di ReturnType
. La parola chiave await
viene utilizzata per attendere che la promessa venga risolta prima di passare alla riga successiva di codice.
Il blocco try/catch
viene utilizzato per gestire eventuali errori che si verificano durante l’esecuzione del codice all’interno della funzione asincrona. Se si verifica un errore, verrà catturato dal blocco catch, dove puoi gestirlo in modo appropriato.
Utilizzando le Funzioni Freccia con Async / Await
Puoi anche utilizzare le funzioni freccia con la sintassi async/await in TypeScript:
const functionName = async (): Promise<ReturnType> => {
try {
const result = await promise;
// codice da eseguire dopo la risoluzione della promessa
return result;
} catch (error) {
// codice da eseguire se la promessa viene rifiutata
throw error;
}
};
Nell’esempio sopra, functionName
è definita come una funzione freccia che restituisce una Promise di ReturnType
. La parola chiave async indica che si tratta di una funzione asincrona, e la parola chiave await viene utilizzata per attendere che la promessa venga risolta prima di passare alla riga successiva di codice.
Async / Await con una chiamata API
Ora, andiamo oltre la sintassi e recuperiamo alcuni dati da un’API utilizzando async/await.
interface User {
id: number;
name: string;
email: string;
}
const fetchApi = async (): Promise<void> => {
try {
const response = await fetch("https://jsonplaceholder.typicode.com/users");
if (!response.ok) {
throw new Error(
`Failed to fetch users (HTTP status code: ${response.status})`
);
}
const data: User[] = await response.json();
console.log(data);
} catch (error) {
console.error(error);
throw error;
}
};
fetchApi();
Qui, stiamo recuperando dati dall’API JSONPlaceholder, convertendoli in JSON e poi registrandoli sulla console. Questo è un esempio del mondo reale di come utilizzare async/await in TypeScript.
Dovresti vedere le informazioni dell’utente nella console. Questa immagine mostra l’output:
Async/Await con chiamata API Axios
// Esempio 2 su come utilizzare async / await in typescript
const fetchApi = async (): Promise<void> => {
try {
const response = await axios.get(
"https://jsonplaceholder.typicode.com/users"
);
const data = await response.data;
console.log(data);
} catch (error) {
console.error(error);
}
};
fetchApi();
Nell’esempio sopra, definiamo la funzione fetchApi()
utilizzando async/await e il metodo Axios.get()
per effettuare una richiesta HTTP GET all’URL specificato. Utilizziamo await per attendere la risposta, quindi estraiamo i dati utilizzando la proprietà data dell’oggetto di risposta. Infine, registriamo i dati sulla console con console.log()
. Eventuali errori che si verificano vengono catturati e registrati sulla console con console.error()
.
Possiamo ottenere questo utilizzando Axios, quindi dovresti vedere lo stesso risultato nella console.
Questa immagine mostra l’output quando si utilizza Axios nella console:
Nota: Prima di provare il codice sopra, è necessario installare Axios utilizzando npm o yarn.
npm install axios
yarn add axios
Se non sei familiare con Axios, puoi scoprire di più qui.
Puoi vedere che abbiamo utilizzato un blocco try
e catch
per gestire gli errori. Il blocco try
e catch
è un metodo per gestire gli errori in TypeScript. Quindi, ogni volta che effettui chiamate API come abbiamo appena fatto, assicurati di utilizzare un blocco try
e catch
per gestire eventuali errori.
Ora, esploriamo un utilizzo più avanzato del blocco try
e catch
in TypeScript:
// Esempio 3 su come usare async / await in typescript
interface Recipe {
id: number;
name: string;
ingredients: string[];
instructions: string[];
prepTimeMinutes: number;
cookTimeMinutes: number;
servings: number;
difficulty: string;
cuisine: string;
caloriesPerServing: number;
tags: string[];
userId: number;
image: string;
rating: number;
reviewCount: number;
mealType: string[];
}
const fetchRecipes = async (): Promise<Recipe[] | string> => {
const api = "https://dummyjson.com/recipes";
try {
const response = await fetch(api);
if (!response.ok) {
throw new Error(`Failed to fetch recipes: ${response.statusText}`);
}
const { recipes } = await response.json();
return recipes; // Restituisci l'array delle ricette
} catch (error) {
console.error("Error fetching recipes:", error);
if (error instanceof Error) {
return error.message;
}
return "An unknown error occurred.";
}
};
// Recupera e registra le ricette
fetchRecipes().then((data) => {
if (Array.isArray(data)) {
console.log("Recipes fetched successfully:", data);
} else {
console.error("Error message:", data);
}
});
Nell’esempio sopra, definiamo un interface Recipe
che delinea la struttura dei dati che ci aspettiamo dall’API. Creiamo quindi la funzione fetchRecipes()
utilizzando async/await e il metodo fetch() per effettuare una richiesta HTTP GET all’endpoint API specificato.
Utilizziamo un blocco try/catch
per gestire eventuali errori che potrebbero verificarsi durante la richiesta API. Se la richiesta ha successo, estraiamo la proprietà dei dati dalla risposta utilizzando await e la restituiamo. Se si verifica un errore, verifichiamo se esiste un messaggio di errore e lo restituiamo come stringa se esiste.
Infine, chiamiamo la funzione fetchRecipes()
e utilizziamo .then()
per registrare i dati restituiti nella console. Questo esempio dimostra come utilizzare async/await
con i blocchi try/catch
per gestire gli errori in uno scenario più avanzato, dove dobbiamo estrarre i dati da un oggetto di risposta e restituire un messaggio di errore personalizzato.
Questa immagine mostra il risultato dell’output del codice:
Async / Await con Promise.all
Promise.all()
è un metodo che accetta un array di promesse come input (un iterabile) e restituisce una singola Promise come output. Questa Promise si risolve quando tutte le promesse di input sono state risolte o se l’iterabile di input non contiene promesse. Si rifiuta immediatamente se una delle promesse di input viene rifiutata o se le non-promesse generano un errore, e si rifiuterà con il primo messaggio di rifiuto o errore.
// Esempio di utilizzo di async / await con Promise.all
interface User {
id: number;
name: string;
email: string;
profilePicture: string;
}
interface Post {
id: number;
title: string;
body: string;
}
interface Comment {
id: number;
postId: number;
name: string;
email: string;
body: string;
}
const fetchApi = async <T>(url: string): Promise<T> => {
try {
const response = await fetch(url);
if (response.ok) {
const data = await response.json();
return data;
} else {
throw new Error(`Network response was not ok for ${url}`);
}
} catch (error) {
console.error(error);
throw new Error(`Error fetching data from ${url}`);
}
};
const fetchAllApis = async (): Promise<[User[], Post[], Comment[]]> => {
try {
const [users, posts, comments] = await Promise.all([
fetchApi<User[]>("https://jsonplaceholder.typicode.com/users"),
fetchApi<Post[]>("https://jsonplaceholder.typicode.com/posts"),
fetchApi<Comment[]>("https://jsonplaceholder.typicode.com/comments"),
]);
return [users, posts, comments];
} catch (error) {
console.error(error);
throw new Error("Error fetching data from one or more APIs");
}
};
fetchAllApis()
.then(([users, posts, comments]) => {
console.log("Users: ", users);
console.log("Posts: ", posts);
console.log("Comments: ", comments);
})
.catch((error) => console.error(error));
Nel codice sopra, abbiamo utilizzato Promise.all
per recuperare più API contemporaneamente. Se hai diverse API da recuperare, puoi utilizzare Promise.all
per ottenerle tutte in una volta. Come puoi vedere, abbiamo utilizzato map
per scorrere l’array delle API e poi passarle a Promise.all
per recuperarle simultaneamente.
L’immagine qui sotto mostra l’output delle chiamate API:
Vediamo come utilizzare Promise.all
con Axios:
// Esempio di utilizzo di async / await con axios e Promise.all
const fetchApi = async () => {
try {
const urls = [
"https://jsonplaceholder.typicode.com/users",
"https://jsonplaceholder.typicode.com/posts",
];
const responses = await Promise.all(urls.map((url) => axios.get(url)));
const data = await Promise.all(responses.map((response) => response.data));
console.log(data);
} catch (error) {
console.error(error);
}
};
fetchApi();
Nell’esempio sopra, stiamo usando Promise.all
per recuperare dati da due URL diversi contemporaneamente. Prima, creiamo un array di URL, poi utilizziamo la mappa per creare un array di Promesse dalle chiamate axios.get
. Passiamo questo array a Promise.all
, che restituisce un array di risposte. Infine, utilizziamo di nuovo la mappa per ottenere i dati da ciascuna risposta e registrarli nella console.
Come utilizzare i callback in TypeScript
Un callback è una funzione passata come argomento a un’altra funzione. La funzione di callback viene eseguita all’interno dell’altra funzione. I callback garantiscono che una funzione non venga eseguita prima che un’attività sia completata – ma che venga eseguita subito dopo il completamento dell’attività. Ci aiutano a scrivere codice JavaScript asincrono e a prevenire problemi ed errori.
// Esempio di utilizzo dei callback in TypeScript
const add = (a: number, b: number, callback: (result: number) => void) => {
const result = a + b;
callback(result);
};
add(10, 20, (result) => {
console.log(result);
});
L’immagine qui sotto mostra la funzione di callback:
Vediamo un altro esempio di utilizzo dei callback in TypeScript:
// Esempio di utilizzo di una funzione di callback in TypeScript
type User = {
name: string;
email: string;
};
const fetchUserData = (
id: number,
callback: (error: Error | null, user: User | null) => void
) => {
const api = `https://jsonplaceholder.typicode.com/users/${id}`;
fetch(api)
.then((response) => {
if (response.ok) {
return response.json();
} else {
throw new Error("Network response was not ok.");
}
})
.then((data) => {
const user: User = {
name: data.name,
email: data.email,
};
callback(null, user);
})
.catch((error) => {
callback(error, null);
});
};
// Utilizzo di fetchUserData con una funzione di callback
fetchUserData(1, (error, user) => {
if (error) {
console.error(error);
} else {
console.log(user);
}
});
Nell’esempio sopra, abbiamo una funzione chiamata fetchUserData
che prende un id
e un callback
come parametri. Questo callback
è una funzione con due parametri: un errore e un utente.
La funzione fetchUserData
recupera i dati dell’utente da un endpoint dell’API JSONPlaceholder utilizzando l’id
. Se il recupero ha successo, crea un oggetto User
e lo passa alla funzione di callback con un errore nullo. Se si verifica un errore durante il recupero, invia l’errore alla funzione di callback con un utente nullo.
Per utilizzare la funzione fetchUserData
con una callback, forniamo un id
e una funzione di callback come argomenti. La funzione di callback controlla gli errori e registra i dati dell’utente se non ci sono errori.
L’immagine qui sotto mostra l’output delle chiamate API:
Come Utilizzare le Callback Responsabilmente
Pur essendo fondamentali per la programmazione asincrona in TypeScript, le callback richiedono una gestione attenta per evitare il “callback hell” – il codice fortemente nidificato a forma di piramide che diventa difficile da leggere e mantenere. Ecco come utilizzare le callback in modo efficace:
-
Evitare la nidificazione profonda
-
Appiattire la struttura del codice suddividendo le operazioni complesse in funzioni denominate
-
Utilizzare promesse o async/await per flussi di lavoro asincroni complessi (più informazioni di seguito)
-
-
Gestione degli errori prima
-
Segui sempre la convenzione di Node.js dei parametri
(errore, risultato)
-
Controlla gli errori a ogni livello di callback annidati
-
function processData(input: string, callback: (err: Error | null, result?: string) => void) {
// ... chiama sempre il callback con l'errore per primo
}
-
Usa annotazioni di tipo
-
Sfrutta il sistema di tipi di TypeScript per imporre le firme dei callback
-
Definisci interfacce chiare per i parametri dei callback
-
type ApiCallback = (error: Error | null, data?: ApiResponse) => void;
-
Considera librerie di controllo del flusso
Per operazioni asincrone complesse, utilizza utilità comeasync.js
per:-
Esecuzione parallela
-
Esecuzione in serie
-
Pipelines per la gestione degli errori
-
Quando utilizzare i callback rispetto alle alternative
Ci sono momenti in cui i callback sono una scelta ottima e altri in cui non lo sono.
I callback sono utili quando si lavora con operazioni asincrone (completamento singolo), interfacciandosi con librerie più vecchie o API che si aspettano callback, gestendo listener di eventi (come listener di click o eventi websocket) o creando utility leggere con esigenze asincrone semplici.
In altri scenari in cui è necessario concentrarsi sulla scrittura di codice manutenibile con un flusso asincrono chiaro, i callback causano problemi e si dovrebbero preferire le promesse o async-await. Ad esempio, quando è necessario concatenare più operazioni, gestire la propagazione degli errori complessi, lavorare con API moderne (come l’API Fetch o FS Promises) o utilizzare promise.all()
per l’esecuzione parallela.
Esempio di migrazione da callback a promesse:
// Versione con callback
function fetchUser(id: number, callback: (err: Error | null, user?: User) => void) {
// ...
}
// Versione con Promise
async function fetchUserAsync(id: number): Promise<User> {
// ...
}
// Utilizzo con async/await
try {
const user = await fetchUserAsync(1);
} catch (error) {
// Gestire l'errore
}
Evoluzione dei Modelli Async
Modello | Vantaggi | Svantaggi |
Callback | Simple, universali | Complessità annidata |
Promise | Catena, migliore flusso degli errori | Richiede catene .then() |
Async/Await | Leggibilità simile alla sincronizzazione | Richiede traspilazione |
I progetti moderni in TypeScript utilizzano spesso un mix: callback per modelli basati su eventi e promise/async-await per logiche asincrone complesse. La chiave è scegliere lo strumento giusto per il tuo caso d’uso specifico mantenendo la chiarezza del codice.
Conclusione
In questo articolo, abbiamo appreso i diversi modi per gestire il codice asincrono in TypeScript. Abbiamo appreso riguardo ai callback, alle promise, a async/await e come usarli in TypeScript. Abbiamo anche appreso questo concetto.
Se vuoi saperne di più sulla programmazione e su come diventare un miglior ingegnere del software, puoi iscriverti al mio canale YouTube CliffTech.
Grazie per aver letto il mio articolo. Spero ti sia piaciuto. Se hai domande, non esitare a contattarmi.
Connettiti con me sui social media: