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

De ce nu mai folosesc boolean-uri pentru statusuri: Discriminated Unions în TypeScript

De Cristian Barbu, 14 iun. 2026 · 17 vizualizări · 2 like-uri

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

function renderUI<T>(state: ApiResponse<T>) {
  switch (state.status) {
    case 'idle': 
      return 'Așteptare...';
    case 'loading': 
      return 'Se încarcă...';
    case 'success': 
      return `Date actualizate la ${state.updatedAt.toLocaleTimeString()}: ${JSON.stringify(state.data)}`;
    case 'error': 
      return `Eroare: ${state.error.message}`;
  }
}

Salutare. Astăzi vreau să vorbim despre cum mapăm stările în TypeScript fără să ne împușcăm în picior. Dacă încă folosești combinații de booleene pentru a gestiona starea unui API sau a unui reducer, probabil ai dat deja de bug-uri greu de reprodus.

Coșmarul lui isLoading și isError

La un proiect mai vechi, o aplicație de monitorizare cu vreo 15.000 de utilizatori activi, aveam cod plin de chestii de genul const [loading, setLoading] = useState(false) combinat cu const [error, setError] = useState(null). Sună cunoscut, nu?

Problema apare când UI-ul ajunge în stări care nu ar trebui să existe din punct de vedere logic. Ce se întâmplă dacă loading e true, dar ai și data din cache, iar error e și el populat pentru că o cerere anterioară a eșuat? Compilerul nu te ajută cu nimic aici. Ai stări invalide care rulează nestingherite în producție și te trezești cu tichete de suport de tipul "ecranul e blocat în loading dar văd și eroarea în spate".

Cum ne salvează Discriminated Unions

Soluția pe care o aplic acum în orice proiect, fie că e vorba de React reducers, răspunsuri de API sau mașini de stare simple, este să folosesc pattern-ul de discriminated union (sau tagged union). Practic, forțăm TypeScript să accepte doar stări valide din punct de vedere logic folosind un literal tipizat ca „discriminator”.

În loc de un obiect mare cu proprietăți opționale, definim tipuri stricte pentru fiecare stare posibilă.

Când starea este 'error', câmpul error este garantat să existe, iar data nici nu poate fi accesat. Când starea este 'success', avem data și știm sigur că nu avem eroare. Am eliminat complet posibilitatea de a avea stări contradictorii.

Unde strălucește pattern-ul ăsta și care sunt compromisurile

Am folosit abordarea asta la un refactoring masiv pe un flux de checkout complex. Am redus bug-urile legate de starea UI-ului cu aproape 40% în prima lună după release. Compilerul pur și simplu nu ne mai lăsa să uităm de cazul de eroare sau să afișăm date incomplete.

Totuși, hai să fim sinceri și să vorbim despre trade-offs. Nu e totul perfect:

  • Boilerplate: Trebuie să scrii mai multe tipuri. Pentru un API simplu de tip "fetch and forget", s-ar putea să ți se pară overkill să definești 4 interfețe diferite doar pentru a randa o listă.
  • Destructuring: Nu mai poți face destructuring ușor la începutul componentei. Dacă încerci const { data, error } = state, TypeScript va urla pentru că în starea de loading, data nu există. Trebuie să folosești structuri condiționale (if sau switch) înainte de a accesa datele specifice unei stări.

Pentru mine, siguranța pe care o primesc la refactoring depășește cu mult disconfortul de a nu putea face destructuring direct.

Voi cum gestionați stările astea? Mergeți pe varianta clasică cu booleene sau ați trecut deja la unelte mai stricte gen discriminated unions sau chiar XState?

Răspunsuri 0

Se încarcă răspunsurile…

Loghează-te pentru a răspunde

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