eduardweb.
TypeScript avansatIntermediar#architecture#typescript#state-management#clean-code

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

De Sorin Tudor, 20 iun. 2026 · 14 vizualizări · 2 like-uri

Postat 20 iun. 2026
typescript
type FetchState<T> =
  | { status: 'idle' }
  | { status: 'loading' }
  | { status: 'success'; data: T; updatedAt: number }
  | { status: 'error'; error: Error };

function renderUI<T>(state: FetchState<T>) {
  switch (state.status) {
    case 'idle':
      return 'Așteaptă inițializarea...';
    case 'loading':
      return 'Se încarcă datele...';
    case 'success':
      // TypeScript știe sigur că state are proprietatea data aici
      return `Date actualizate la ${state.updatedAt}: ${JSON.stringify(state.data)}`;
    case 'error':
      // TypeScript știe sigur că state are error aici
      return `Eroare: ${state.error.message}`;
  }
}

Să fim sinceri: de câte ori ai dat de un bug în producție pentru că ai uitat să pui isLoading = false când s-a declanșat o eroare? Eu am pățit-o de destule ori în primii ani de frontend până când m-am prins că boolean-urile împrăștiate prin stare sunt o capcană auto-impusă. Soluția pe care o folosesc de ani de zile în TypeScript se numește discriminated unions (sau tagged unions) și îmi salvează nopțile.

La un proiect trecut, o aplicație de tip dashboard cu vreo 14.000 de utilizatori activi pe zi, aveam un flow destul de complex de checkout și onboarding. Inițial, starea arăta clasic: loading: boolean, error?: string, data?: UserData. Sună cunoscut, nu? Problema e că tipul ăsta de structură îți permite stări imposibile în mod nativ. Poți avea loading: true ȘI data populat în același timp, deși logic n-are niciun sens. Compilerul nu zicea nimic, dar UI-ul o lua razna în cazuri de rețea instabilă.

Când am trecut totul pe uniuni discriminate, am eliminat din start aceste stări invalide. Practic, forțezi TypeScript să înțeleagă că dacă starea are un anumit tag, restul proprietăților sunt strict limitate la acel context.

Unde strălucește pattern-ul ăsta în mod special

Folosesc acest pattern în trei locuri sfinte din aplicațiile mele:

  1. State-ul componentelor sau reducerilor: În loc de o găleată de variabile, am un singur obiect de stare definit clar prin stări exclusive (idle, loading, success, error).
  2. Răspunsurile de la API: În loc să returnez un obiect generic cu chestii opționale, definesc clar succesul și eșecul ca două tipuri distincte cu un discriminant comun, cum ar fi ok: true sau ok: false.
  3. Acțiunile din Reducer (sau useReducer): E un clasic, dar mulți uită să folosească tipuri stricte pe payload-uri în funcție de tipul acțiunii.

Trade-off-ul sincer

Nimic nu-i gratuit în programare. Trade-off-ul major aici este verbozitatea. Trebuie să scrii mai mult cod boilerplate pentru a defini fiecare tip în parte.

Uneori, când ai uniuni foarte mari, autocomplete-ul din IDE sau compilerul TypeScript (tsc) poate deveni vizibil mai lent. Am observat asta la un proiect uriaș cu peste 200 de acțiuni de Reducer grupate într-un singur loc. De asemenea, dacă lucrezi cu juniori în echipă, s-ar putea să se chinuie puțin la început cu pattern matching-ul manual folosind switch sau if pe post de type guards.

Totuși, să ai erori prinse direct în IDE înainte ca acel cod să ajungă în staging mi se pare un câștig imens. Am redus bug-urile pe partea de UI state cu cel puțin 35% după ce am adoptat regula asta în echipă.

Voi cum gestionați stările imposibile în aplicațiile mari? Mergeți pe unions sau preferați o abordare bazată pe validări la runtime?

Răspunsuri 0

Se încarcă răspunsurile…

Loghează-te pentru a răspunde

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