Programare asincronă în TypeScript
TypeScript adaugă tipuri statice funcționalităților asincrone din JavaScript. Astfel, codul asincron devine mai ușor de anticipat și de întreținut.
Această lecție pornește de la async/await de bază și ajunge la modele mai avansate.
Notă: Această lecție presupune că știi deja bazele Promise-urilor din JavaScript și ale programării asincrone. Dacă aceste idei sunt noi pentru tine, învață mai întâi despre async în JavaScript.
Promise-uri în TypeScript
TypeScript face Promise-urile sigure din punctul de vedere al tipurilor, prin generice.
Un Promise<T> este o operațiune care se va încheia mai târziu cu o valoare de tip T sau va eșua cu un motiv de tip any.
Puncte importante:
Promise<T>— tip generic în careTeste tipul valorii rezolvate.Promise<void>— pentru Promise-uri care nu întorc o valoare.Promise<never>— pentru Promise-uri care nu se rezolvă niciodată (rar).
Stările unui Promise și fluxul tipurilor: un Promise pornește în starea pending și trece fie în fulfilled (cu o valoare de tip T), fie în rejected (cu un motiv de tip any). TypeScript urmărește aceste stări prin sistemul de tipuri, așa că tratezi atât cazul de succes, cât și cazul de eroare.
Parametrul de tip din Promise<T> îi spune lui TypeScript la ce se va rezolva Promise-ul, ceea ce oferă o verificare a tipurilor mai bună și un suport mai bun în editor.
Exemplu de bază cu Promise
fetchGreeting întoarce Promise<string>, așa că în interiorul lui .then TypeScript știe că greeting este un șir de caractere. Promise-ul se rezolvă sau se respinge aleatoriu, așa că rezultatul este fie salutul scris cu majuscule, fie linia cu eroarea.
fetchGreeting, tipizată să întoarcă Promise<string>. În interiorul ei, construiește un Promise care așteaptă o secundă cu setTimeout, apoi, aleatoriu (folosind Math.random() > 0.5), fie se rezolvă cu "Salut, TypeScript!", fie se respinge cu Error("Preluarea salutului a eșuat").
Consumă-l cu .then — afișând salutul scris cu majuscule, de vreme ce TypeScript știe că este un șir de caractere — și cu .catch, având eroarea tipizată ca Error, afișând "Eroare:" și mesajul ei. Rezultatul este aleatoriu: fie salutul scris cu majuscule, fie linia cu eroarea.
Ieșire: program TypeScript și rezultatul capturat
// Creăm un Promise tipizat care se rezolvă cu un șir de caractere
const fetchGreeting = (): Promise<string> => {
return new Promise((resolve, reject) => {
setTimeout(() => {
const success = Math.random() > 0.5;
if (success) {
resolve("Salut, TypeScript!");
} else {
reject(new Error("Preluarea salutului a eșuat"));
}
}, 1000);
});
};
// Folosim Promise-ul cu inferență corectă a tipurilor
fetchGreeting()
.then((greeting) => {
// TypeScript știe că „greeting” este un șir de caractere
console.log(greeting.toUpperCase());
})
.catch((error: Error) => {
console.error("Eroare:", error.message);
});
Async/Await cu TypeScript
Sintaxa async/await este un mod mai curat de a lucra cu Promise-uri. Codul asincron se citește ca un cod obișnuit, pas cu pas, iar TypeScript continuă să verifice tipurile.
Principalele avantaje ale async/await:
- Lizibilitate: cod secvențial, mai ușor de urmărit.
- Tratarea erorilor: folosești
try/catchatât pentru erorile sincrone, cât și pentru cele asincrone. - Depanare: urmele de stivă arată ca cele sincrone.
- Siguranța tipurilor: inferență și verificare complete în TypeScript.
Tipurile de retur ale funcțiilor async. Orice funcție async întoarce un Promise, iar tipul de retur declarat trebuie să fie învelit într-unul: async function getString(): string { } este o eroare, în timp ce async function getString(): Promise<string> { } este corect.
Notă: Parametrul de tip al Promise-ului întors este tipul pe care îl scrii după Promise în tipul de retur.
Exemplu de bază cu async/await
fetchUsers întoarce Promise<User[]>, așa că după await variabila users este de tip User[], iar accesul la proprietățile fiecărui utilizator este verificat de tipuri.
User (id, name, email și role limitat la 'admin' | 'user' | 'guest') și o async function fetchUsers(): Promise<User[]> care afișează 'Se preiau utilizatorii...', așteaptă cu await un setTimeout de o secundă pentru a simula un apel de API, apoi întoarce pe Alice (admin) și pe Bob (user).
Scrie async function processUsers() care așteaptă cu await rezultatul lui fetchUsers() într-un try/catch, afișează câți utilizatori au fost preluați, filtrează administratorii și îi transformă în adresele lor de email, afișează 'Emailurile administratorilor:' împreună cu acel tablou și rearuncă orice eroare după ce o afișează (verificând error instanceof Error). La final, apelează processUsers() și afișează 'Procesare finalizată' în caz de succes sau 'Procesarea a eșuat:' în caz de eșec.
Ieșire: program TypeScript și rezultatul capturat
// Definim tipurile pentru răspunsul API-ului nostru
interface User {
id: number;
name: string;
email: string;
role: 'admin' | 'user' | 'guest';
}
// Funcție care întoarce un Promise cu un tablou de User
async function fetchUsers(): Promise<User[]> {
console.log('Se preiau utilizatorii...');
// Simulăm un apel de API
await new Promise(resolve => setTimeout(resolve, 1000));
return [
{ id: 1, name: 'Alice', email: '[email protected]', role: 'admin' },
{ id: 2, name: 'Bob', email: '[email protected]', role: 'user' }
];
}
// Funcție asincronă pentru procesarea utilizatorilor
async function processUsers() {
try {
// TypeScript știe că users este User[]
const users = await fetchUsers();
console.log(`S-au preluat ${users.length} utilizatori`);
// Acces la proprietăți verificat de tipuri
const adminEmails = users
.filter(user => user.role === 'admin')
.map(user => user.email);
console.log('Emailurile administratorilor:', adminEmails);
return users;
} catch (error) {
if (error instanceof Error) {
console.error('Procesarea utilizatorilor a eșuat:', error.message);
} else {
console.error('A apărut o eroare necunoscută');
}
throw error; // Rearuncăm eroarea pentru ca apelantul să o trateze
}
}
// Executăm funcția asincronă
processUsers()
.then(users => console.log('Procesare finalizată'))
.catch(err => console.error('Procesarea a eșuat:', err));
Execuție paralelă cu Promise.all
Rulează mai multe operațiuni asincrone în paralel și așteaptă ca toate să se încheie. Cele trei produse sunt preluate în același timp, iar prețurile lor aleatorii sunt adunate.
Product (id, name, price) și o async function fetchProduct(id: number): Promise<Product> care afișează Se preia produsul <id>..., așteaptă un timp aleatoriu de până la o secundă și întoarce un produs numit Produsul <id>, cu un preț aleatoriu, număr întreg, mai mic de 100.
Scrie fetchMultipleProducts(), care pornește fetchProduct(1), (2) și (3) în paralel cu Promise.all, destructurează cele trei rezultate, adună prețurile lor și afișează Preț total: $<total cu două zecimale>; afișează orice eroare cu 'Eroare la preluarea produselor:'. Apelează funcția. Totalul este aleatoriu.
Ieșire: program TypeScript și rezultatul capturat
interface Product {
id: number;
name: string;
price: number;
}
async function fetchProduct(id: number): Promise<Product> {
console.log(`Se preia produsul ${id}...`);
await new Promise(resolve => setTimeout(resolve, Math.random() * 1000));
return { id, name: `Produsul ${id}`, price: Math.floor(Math.random() * 100) };
}
async function fetchMultipleProducts() {
try {
// Pornim toate preluările în paralel
const [product1, product2, product3] = await Promise.all([
fetchProduct(1),
fetchProduct(2),
fetchProduct(3)
]);
const total = [product1, product2, product3]
.reduce((sum, product) => sum + product.price, 0);
console.log(`Preț total: $${total.toFixed(2)}`);
} catch (error) {
console.error('Eroare la preluarea produselor:', error);
}
}
fetchMultipleProducts();
Tipizarea callback-urilor pentru operațiuni asincrone
Pentru codul asincron mai vechi, scris în stil callback, TypeScript te ajută să tipizezi parametrii callback-ului, astfel încât apelantul să știe exact ce primește.
Exemplu
Tipul FetchCallback spune că acel callback primește un Error sau null, plus date opționale de tip șir de caractere. În interiorul callback-ului, TypeScript știe că data este un șir de caractere sau undefined.
FetchCallback pentru un callback (error: Error | null, data?: string) => void și o funcție fetchDataWithCallback(url: string, callback: FetchCallback): void care simulează o operațiune asincronă cu un setTimeout de o secundă, apelând callback-ul cu null și "Date de răspuns" (sau cu un Error, dacă apare o excepție).
Apeleaz-o cu 'https://api.example.com' și cu un callback care afișează 'Eroare:' și se întoarce în caz de eroare, iar altfel afișează datele scrise cu majuscule atunci când sunt definite.
Ieșire: program TypeScript și rezultatul capturat
// Definim un tip pentru callback
type FetchCallback = (error: Error | null, data?: string) => void;
// Funcție care primește un callback tipizat
function fetchDataWithCallback(url: string, callback: FetchCallback): void {
// Simulăm o operațiune asincronă
setTimeout(() => {
try {
// Simulăm un răspuns reușit
callback(null, "Date de răspuns");
} catch (error) {
callback(error instanceof Error ? error : new Error('Eroare necunoscută'));
}
}, 1000);
}
// Folosirea funcției cu callback
fetchDataWithCallback('https://api.example.com', (error, data) => {
if (error) {
console.error('Eroare:', error.message);
return;
}
// TypeScript știe că data este un șir de caractere (sau undefined)
if (data) {
console.log(data.toUpperCase());
}
});
Combinații de Promise-uri
TypeScript îți oferă versiuni tipizate ale metodelor care combină mai multe Promise-uri. Ele te ajută să rulezi operațiuni concurent și să tratezi rezultatele în siguranță.
Metode de combinare a Promise-urilor:
Promise.all()— așteaptă ca toate promise-urile să se rezolve.Promise.race()— întoarce primul promise stabilizat.Promise.allSettled()— așteaptă ca toate să se stabilizeze (succes sau eșec).Promise.any()— întoarce primul promise îndeplinit.
Atenție: Când îi dai lui Promise.all promise-uri de tipuri diferite, TypeScript deduce un tuplu cu tipurile rezultatelor individuale. Dacă tipul dedus nu este destul de precis, folosește o aserțiune de tip sau definește tu structura așteptată.
Promise.all - execuție paralelă
Rulează mai multe promise-uri în paralel și le așteaptă pe toate. Eșuează imediat dacă vreun promise este respins. Aici user, posts și stats primesc fiecare propriul tip precis din tuplul rezultat.
fetchUser(id), care dă { id, name: "Utilizatorul <id>" }, fetchPosts(userId), care dă două postări (Postarea 1, Postarea 2) cu id-uri, și fetchStats(userId), care dă { views: 100, likes: 25 }.
Scrie async function loadUserDashboard(userId: number) care așteaptă cu await toate cele trei prin Promise.all, afișează Utilizator: <name>, Postări: <count> și Aprecieri: <likes>, întoarce cele trei valori într-un obiect și afișează 'Încărcarea tabloului de bord a eșuat:' înainte de a rearunca eroarea. Apeleaz-o cu id-ul de utilizator 1.
Ieșire: program TypeScript și rezultatul capturat
// Tipuri diferite de promise-uri
const fetchUser = (id: number): Promise<{ id: number; name: string }> =>
Promise.resolve({ id, name: `Utilizatorul ${id}` });
const fetchPosts = (userId: number): Promise<Array<{ id: number; title: string }>> =>
Promise.resolve([
{ id: 1, title: 'Postarea 1' },
{ id: 2, title: 'Postarea 2' }
]);
const fetchStats = (userId: number): Promise<{ views: number; likes: number }> =>
Promise.resolve({ views: 100, likes: 25 });
// Rulăm toate în paralel
async function loadUserDashboard(userId: number) {
try {
const [user, posts, stats] = await Promise.all([
fetchUser(userId),
fetchPosts(userId),
fetchStats(userId)
]);
// TypeScript știe tipurile pentru user, posts și stats
console.log(`Utilizator: ${user.name}`);
console.log(`Postări: ${posts.length}`);
console.log(`Aprecieri: ${stats.likes}`);
return { user, posts, stats };
} catch (error) {
console.error('Încărcarea tabloului de bord a eșuat:', error);
throw error;
}
}
// Executăm cu un ID de utilizator
loadUserDashboard(1);
Promise.race - primul stabilizat
Util pentru expirări sau pentru a lua primul răspuns reușit dintre mai multe surse. fetchWithTimeout pune un promise în cursă cu un cronometru care respinge. Funcțiile sunt doar definite, așa că nu se afișează nimic.
timeout(ms: number): Promise<never> care se respinge cu Error("Expirare după <ms>ms") după ms milisecunde, și o funcție generică async function fetchWithTimeout<T>(promise: Promise<T>, timeoutMs = 5000): Promise<T> care pune promise-ul dat în cursă cu expirarea, aruncând Cererea a expirat după <ms>ms dacă expirarea câștigă.
Apoi definește (dar nu apela) fetchUserData(), care folosește fetchWithTimeout în jurul lui fetch('https://api.example.com/user/1') cu o limită de 3 secunde, parsează JSON-ul, iar în caz de eroare afișează 'Eroare:' împreună cu mesajul (convertit la Error) și rearuncă eroarea. Nu se afișează nimic, pentru că nicio funcție nu este apelată.
Ieșire: program TypeScript și rezultatul capturat
// Funcție ajutătoare pentru expirare
const timeout = (ms: number): Promise<never> =>
new Promise((_, reject) =>
setTimeout(() => reject(new Error(`Expirare după ${ms}ms`)), ms)
);
// Simulăm un apel de API cu expirare
async function fetchWithTimeout<T>(
promise: Promise<T>,
timeoutMs: number = 5000
): Promise<T> {
return Promise.race([
promise,
timeout(timeoutMs).then(() => {
throw new Error(`Cererea a expirat după ${timeoutMs}ms`);
}),
]);
}
// Exemplu de utilizare
async function fetchUserData() {
try {
const response = await fetchWithTimeout(
fetch('https://api.example.com/user/1'),
3000 // expirare după 3 secunde
);
const data = await response.json();
return data;
} catch (error) {
console.error('Eroare:', (error as Error).message);
throw error;
}
}
Promise.allSettled - tratarea tuturor rezultatelor
Folosește-l când vrei să aștepți fiecare promise, fie că reușește, fie că eșuează. Fiecare rezultat este verificat cu un predicat de tip înainte de a fi numărat ca reușit sau eșuat. Unele cereri eșuează aleatoriu, așa că numerele variază.
fetchData(id: number) care aruncă Error("Preluarea datelor pentru ID-ul <id> a eșuat") atunci când Math.random() > 0.7 și altfel întoarce { id, data: "Date pentru <id>" }.
Scrie async function processBatch(ids: number[]) care transformă fiecare id în fetchData(id), învelit astfel încât să se rezolve cu { status: 'fulfilled' as const, value } sau cu { status: 'rejected' as const, reason }, așteaptă cu await un Promise.allSettled pe ele, separă rezultatele reușite de cele eșuate prin filtre cu predicate de tip (PromiseFulfilledResult / PromiseRejectedResult), afișează Procesate cu succes: <n> și Eșuate: <n> și întoarce ambele liste. Apeleaz-o cu id-urile de la 1 la 5. Numerele sunt aleatorii.
Ieșire: program TypeScript și rezultatul capturat
// Simulăm mai multe apeluri de API cu rezultate diferite
const fetchData = async (id: number) => {
// Facem ca unele cereri să eșueze aleatoriu
if (Math.random() > 0.7) {
throw new Error(`Preluarea datelor pentru ID-ul ${id} a eșuat`);
}
return { id, data: `Date pentru ${id}` };
};
// Procesăm mai multe elemente, tratând individual erorile
async function processBatch(ids: number[]) {
const promises = ids.map(id =>
fetchData(id)
.then(value => ({ status: 'fulfilled' as const, value }))
.catch(reason => ({ status: 'rejected' as const, reason }))
);
// Așteptăm ca toate să se încheie
const results = await Promise.allSettled(promises);
// Procesăm rezultatele
const successful = results
.filter((result): result is PromiseFulfilledResult<{ status: 'fulfilled', value: any }> =>
result.status === 'fulfilled' &&
result.value.status === 'fulfilled'
)
.map(r => r.value.value);
const failed = results
.filter((result): result is PromiseRejectedResult |
PromiseFulfilledResult<{ status: 'rejected', reason: any }> => {
if (result.status === 'rejected') return true;
return result.value.status === 'rejected';
});
console.log(`Procesate cu succes: ${successful.length}`);
console.log(`Eșuate: ${failed.length}`);
return { successful, failed };
}
// Procesăm un lot de ID-uri
processBatch([1, 2, 3, 4, 5]);
Tratarea erorilor în codul asincron
TypeScript îți oferă instrumente bune pentru tratarea erorilor în siguranță din punctul de vedere al tipurilor, în codul asincron. Iată câteva modele și bune practici.
Strategii de tratare a erorilor:
- Blocuri try/catch: pentru tratarea erorilor în async/await.
- Error boundaries: pentru componentele React.
- Tipuri de tip Result: o abordare funcțională, cu valori de succes/eșec.
- Subclase de erori: pentru erori specifice domeniului.
Modele de tratare a erorilor pe care merită să le ai în vedere:
- Error boundaries în React, pentru tratare la nivel de componentă.
- Obiecte Result în locul aruncării erorilor pentru cazurile așteptate.
- Gestionar global de erori pentru excepțiile netratate.
- Jurnalizarea erorilor pentru a captura și a raporta erorile.
Clase de eroare personalizate
Creează tipuri de eroare specifice domeniului pentru o tratare mai bună a erorilor. AppError este clasa de bază, iar NetworkError, ValidationError și NotFoundError poartă fiecare un cod. Un mockFetch local ține locul lui fetch, astfel încât exemplul să ruleze fără rețea.
AppError extends Error, al cărei constructor primește message, un public readonly code: string și un public readonly details?: unknown opțional, setează this.name la numele clasei și captează urma stivei atunci când este disponibilă; apoi NetworkError (cod 'NETWORK_ERROR'), ValidationError (cod 'VALIDATION_ERROR', cu un public readonly field) și NotFoundError (cod 'NOT_FOUND', mesaj <resource> cu ID-ul <id> nu a fost găsit).
Scrie async function fetchUserData(userId: string): Promise<{ id: string; name: string }> care așteaptă cu await un mockFetch local (un Promise care se rezolvă cu un răspuns fals { ok, status, json() }, astfel încât rețeaua să nu fie atinsă), aruncă NotFoundError la 404, NetworkError la 5xx, o eroare simplă Error pentru alte statusuri care nu sunt ok și ValidationError('name', ...) atunci când JSON-ul nu are name; erorile neașteptate sunt învelite într-un AppError cu codul 'UNEXPECTED_ERROR'. Adaugă displayUserProfile(userId), care afișează 'Profilul utilizatorului:' împreună cu utilizatorul sau afișează prin console.error un prefix diferit (Problemă de rețea:, Validarea a eșuat:, Nu a fost găsit:, Eroare neașteptată:) pentru fiecare clasă de eroare, și apeleaz-o cu '123'.
Ieșire: program TypeScript și rezultatul capturat
// Clasa de eroare de bază pentru aplicația noastră
class AppError extends Error {
constructor(
message: string,
public readonly code: string,
public readonly details?: unknown
) {
super(message);
this.name = this.constructor.name;
(Error as any).captureStackTrace?.(this, this.constructor);
}
}
// Tipuri de eroare specifice
class NetworkError extends AppError {
constructor(message: string, details?: unknown) {
super(message, 'NETWORK_ERROR', details);
}
}
class ValidationError extends AppError {
constructor(
public readonly field: string,
message: string
) {
super(message, 'VALIDATION_ERROR', { field });
}
}
class NotFoundError extends AppError {
constructor(resource: string, id: string | number) {
super(
`${resource} cu ID-ul ${id} nu a fost găsit`,
'NOT_FOUND',
{ resource, id }
);
}
}
// Înlocuitor local pentru fetch, ca exemplul să nu atingă niciodată rețeaua
const mockFetch = (url: string): Promise<{ ok: boolean; status: number; json: () => Promise<any> }> =>
Promise.resolve({
ok: true,
status: 200,
json: async () => ({ id: url.split('/').pop(), name: 'Alice' })
});
// Exemplu de utilizare
async function fetchUserData(userId: string): Promise<{ id: string; name: string }> {
try {
// Simulăm un apel de API
const response = await mockFetch(`/api/users/${userId}`);
if (!response.ok) {
if (response.status === 404) {
throw new NotFoundError('Utilizatorul', userId);
} else if (response.status >= 500) {
throw new NetworkError('Eroare de server', { status: response.status });
} else {
throw new Error(`Eroare HTTP! status: ${response.status}`);
}
}
const data = await response.json();
// Validăm datele din răspuns
if (!data.name) {
throw new ValidationError('name', 'Numele este obligatoriu');
}
return data;
} catch (error) {
if (error instanceof AppError) {
// Este deja una dintre erorile noastre personalizate
throw error;
}
// Învelim erorile neașteptate
throw new AppError(
'Preluarea datelor utilizatorului a eșuat',
'UNEXPECTED_ERROR',
{ cause: error }
);
}
}
// Tratarea erorilor în aplicație
async function displayUserProfile(userId: string) {
try {
const user = await fetchUserData(userId);
console.log('Profilul utilizatorului:', user);
} catch (error) {
if (error instanceof NetworkError) {
console.error('Problemă de rețea:', error.message);
// Afișăm interfața de reîncercare
} else if (error instanceof ValidationError) {
console.error('Validarea a eșuat:', error.message);
// Evidențiem câmpul invalid
} else if (error instanceof NotFoundError) {
console.error('Nu a fost găsit:', error.message);
// Afișăm pagina 404
} else {
console.error('Eroare neașteptată:', error);
// Afișăm un mesaj de eroare generic
}
}
}
// Executăm cu date de exemplu
displayUserProfile('123');
Iterare asincronă cu TypeScript
TypeScript acceptă iteratori asincroni și generatoare asincrone cu tipizare completă. O buclă for await extrage valorile una câte una, pe măsură ce devin disponibile.
Exemplu
generateNumbers este tipizată ca AsyncGenerator<number, void, unknown>, așa că în interiorul buclei for await variabila num este un număr. Aici funcțiile sunt doar definite, așa că nu se afișează nimic.
generateNumbers() tipizat ca AsyncGenerator<number, void, unknown>, care produce valorile de la 0 la 4, așteptând o secundă (printr-un setTimeout într-un Promise așteptat cu await) înainte de fiecare valoare.
Adaugă async function consumeNumbers(), care folosește for await peste generator și afișează fiecare număr dublat. Doar definește funcțiile; nimic nu este apelat, așa că nu se afișează nimic.
Ieșire: program TypeScript și rezultatul capturat
// Funcție generator asincronă
async function* generateNumbers(): AsyncGenerator<number, void, unknown> {
let i = 0;
while (i < 5) {
// Simulăm o operațiune asincronă
await new Promise(resolve => setTimeout(resolve, 1000));
yield i++;
}
}
// Folosirea generatorului asincron
async function consumeNumbers() {
for await (const num of generateNumbers()) {
// TypeScript știe că num este un număr
console.log(num * 2);
}
}