type AsyncState<T> =
| { status: 'idle' }
| { status: 'loading' }
| { status: 'success'; data: T; fetchedAt: Date }
| { status: 'error'; error: Error };
function renderUI(state: AsyncState<string[]>) {
switch (state.status) {
case 'idle':
return 'Așteaptă acțiunea...';
case 'loading':
return 'Se încarcă datele...';
case 'success':
// TypeScript știe automat că state are .data și .fetchedAt
return `Avem ${state.data.length} elemente (luate la ${state.fetchedAt.toISOString()})`;
case 'error':
// Autocomplete perfect pentru .error.message
return `A apărut o eroare: ${state.error.message}`;
}
}Acum vreo 4 ani lucram la un dashboard financiar unde procesam peste 100k de tranzacții zilnic. Aveam o componentă React care gestiona afișarea datelor și codul arăta groaznic: vreo patru flag-uri de isLoading, isError, data și errorMessage pe același obiect de stare. Am ajuns inevitabil în scenariul unde isLoading era true, dar aveam și data vechi din cache, iar isError devenise și el true dintr-un render anterior.
Atunci am făcut refactoring complet la starea aplicației folosind Discriminated Unions. A fost momentul în care am înțeles că cel mai bun cod e ăla care face stările invalide imposibil de reprezentat la nivel de tipuri.
Ce este, de fapt, un Discriminated Union?
Pe scurt, este o uniune de tipuri de obiecte care au o proprietate comună cu un tip literal (numită de obicei type, kind sau status). TypeScript folosește acea proprietate comună ca un discriminator pentru a deduce exact cu ce tip de obiect lucrezi atunci când faci verificări într-un if sau switch.
În loc să ai o singură interfață gigant cu toate câmpurile opționale (data?: T, error?: string), spargi totul în tipuri atomice, bine definite.
Unde le folosesc zilnic
Am trei cazuri principale unde nu mai scriu cod fără pattern-ul ăsta:
- Stările de încărcare (Async State): În loc de
{ loading: boolean, data: T | null }, folosesc o uniune cu stări explicite (idle,loading,success,error). Cândstatuse'success', TS știe sigur cădataexistă și nu enull. - Reducer Actions (React
useReducersau Redux): Fiecare acțiune are untypeunic. Înswitch, compilerul știe exact ce payload are acțiunea respectivă fără aserțiuni manuale de tip (as SomeType). - Răspunsuri de la API (Result Pattern): Returnez
{ success: true; data: T }sau{ success: false; error: string }. Elimini complet blocuriletry/catchagresive din UI.
Trade-off-uri reale (nimic nu e gratis)
Să fim sinceri, pattern-ul vine și cu dezavantaje. În primul rând, adaugă un pic de boilerplate. Dacă ai un flux cu 10-15 stări sau evenimente mici, numărul de tipuri declarate crește simțitor și trebuie să le întreții.
În al doilea rând, dacă consumi un API legacy care îți trimite obiecte ciudate unde data și error vin amestecate pe același payload HTTP 200, TypeScript nu te poate salva la runtime. Trebuie să treci datele printr-o librărie de validare gen Zod înainte să le poți mapa pe o uniune discriminată.
Totuși, la proiecte mari unde lucrează 5-10 devi pe aceeași bază de cod, beneficiul de DX și reducerea bug-urilor din producție întrec de departe cele câteva linii de cod în plus.
Voi mai folosiți booleene suprapuse pentru stări de loading sau ați trecut complet pe tipare algebrice și state machines?