eduardweb.
TypeScript avansatIntermediar#architecture#typescript#frontend#clean-code

Discriminated Unions în TypeScript: Cum am scăpat de bug-urile de stare în producție

De Ioan Manole, 21 iun. 2026 · 14 vizualizări · 3 like-uri

Postat 21 iun. 2026
typescript
type ApiResponse<T> =
  | { status: 'success'; data: T; timestamp: number }
  | { status: 'error'; error: string; code: number };

function handleResponse(response: ApiResponse<{ name: string }>) {
  if (response.status === 'success') {
    // TypeScript știe sigur că response.data există aici
    console.log(`User validat: ${response.data.name}`);
  } else {
    // Aici response.error este garantat să fie string, iar data nu există
    console.error(`Eroare ${response.code}: ${response.error}`);
  }
}

Salutare tuturor. Am refăcut acum vreo opt luni un flow de checkout destul de complex pentru o aplicație de e-commerce cu vreo 15.000 de utilizatori activi. Codul vechi era o colecție horror de flag-uri: isLoading, isError, hasCoupon, paymentPending. Dacă puneam isLoading pe true, trebuia să am grijă manual să pun isError pe false la finalul request-ului. Evident că mai scăpam chestii și ne trezeam cu ecrane blocate în producție sau loading-uri infinite.

Atunci am decis să rescriem tot state-ul folosind discriminated unions (sau tagged unions). Este probabil cea mai curată metodă din TypeScript de a modela stări excluzive, dar este adesea ignorată de mulți developeri la început de drum.

Ce înseamnă de fapt?

În loc să ai un obiect gigant care încearcă să acopere toate stările posibile prin proprietăți opționale, împarți starea în tipuri distincte. Singura lor legătură este o proprietate comună, un literal de tip string (un "tag"), pe care TypeScript îl folosește ca să facă type narrowing.

Când folosești un switch sau un if pe acel tag, compilatorul știe exact ce alte proprietăți sunt disponibile în acel bloc de cod. Nu mai ai nevoie de type assertions (as MyType) sau de verificări obositoare de tipul if (data !== undefined).

Unde strălucește pattern-ul ăsta

1. Răspunsurile din API

Cea mai mare greșeală pe care o văd este să ai un singur tip pentru răspunsul de la server care conține și datele, și erorile. Ceva generic de genul: { data?: User, error?: string }. Te obligă să pui ? sau ! peste tot prin componente.

Dacă folosești discriminated unions, separi clar succesul de eșec. TypeScript te va obliga să verifici dacă request-ul a reușit înainte să poți accesa data. Am redus bug-urile de tip "undefined is not an object" pe zona de integrare API cu aproape 90% după ce am adoptat abordarea asta.

2. State Machines în React/Redux

Dacă ai un reducer, discriminated unions sunt sfinte. Fiecare acțiune are un type diferit și un payload specific. În plus, starea în sine poate fi o uniune. Nu poți avea data în starea ta dacă starea curentă este LOADING. Pur și simplu compilatorul nu te lasă să scrii codul ăla greșit. Prinzi bug-ul în IDE, nu în producție.

Trade-off-ul sincer

Nimic nu e perfect în meseria asta. Dacă folosești pattern-ul ăsta, o să scrii mai mult boilerplate la început. Trebuie să definești tipuri separate pentru fiecare stare în parte, ceea ce poate părea obositor.

Mai mult, dacă backend-ul tău e vechi și trimite date amestecate (de exemplu, un status de succes dar cu cod de eroare ascuns adânc prin vreun payload), va trebui să scrii controllere de mapare la intrare în aplicație. Nu merge să arunci direct JSON-ul din fetch în state-ul tău frumos tipizat fără o validare anterioară. Pentru noi, efortul de mapare a meritat pe deplin, dar la proiecte mici s-ar putea să pară prea mult overhead.

Cum gestionați stările astea complexe în proiectele voastre? Mergeți pe flag-uri simple sau folosiți mașini de stare stricte?

Răspunsuri 0

Se încarcă răspunsurile…

Loghează-te pentru a răspunde

Doar membrii comunității pot lăsa comentarii.