eduardweb.
TypeScript avansatIntermediar#architecture#typescript#state-management#react

De ce am renunțat la flag-urile booleene pentru Discriminated Unions în TypeScript

De Paul Ene, 6 iun. 2026 · 1 vizualizări · 3 like-uri

Postat acum 3 zile
typescript
type ApiResponse<T> =
  | { status: 'idle' }
  | { status: 'loading' }
  | { status: 'success'; data: T; updatedAt: number }
  | { status: 'error'; error: Error };

function handleResponse(response: ApiResponse<string>) {
  switch (response.status) {
    case 'loading':
      return 'Se încarcă datele...';
    case 'success':
      // TypeScript știe sigur că `data` și `updatedAt` există aici
      return `Date primite: ${response.data.toUpperCase()} la ${response.updatedAt}`;
    case 'error':
      return `Eroare: ${response.error.message}`;
    default:
      return 'Stare inactivă';
  }
}

Salutare tuturor. Dacă încă folosiți structuri de date de tipul { isLoading: boolean, data?: T, error?: string } pentru a gestiona stările asincrone, vă faceți viața grea cu mâna voastră.

Am pățit-o acum vreo trei ani la o aplicație de monitorizare cu peste 12.000 de utilizatori activi, unde ne-am trezit cu ecrane albe în producție din cauza unor stări inconsistente. Genul de bug unde isLoading era false, dar și data și error erau ambele undefined pentru că cineva uitase să trateze un caz de edge-case pe rețea. Soluția salvatoare a fost trecerea la pattern-ul de discriminated unions (sau uniuni etichetate, deși sună destul de academic).

De ce stările booleene sunt o bombă cu ceas

Când pui flag-uri booleene unul lângă altul, creezi un spațiu de stări teoretice mult mai mare decât cel real. Dacă ai trei booleene (isLoading, isError, isSuccess), ai de fapt opt combinații posibile de stări în cod. Dar în realitate, doar trei sau patru dintre ele au sens fizic. Nu poți fi și în loading și în success în același timp.

TypeScript nu are de unde să știe asta dacă îi dai doar opționale. Te trezești că trebuie să pui if (data) peste tot în componente, chiar dacă logica ta de business spune că dacă nu ești în loading sau error, datele trebuie să fie acolo.

Cum curățăm starea cu discriminated unions

Ideea e simplă: definim un singur tip care poate fi în mai multe forme, dar fiecare formă are un câmp comun cu o valoare literală unică (discriminantul). TypeScript folosește acest câmp pentru a face "narrowing" automat în interiorul blocurilor condiționale.

Dacă te uiți la exemplul de cod atașat, o să vezi cum tipul status acționează ca un switch de siguranță pentru compilator. Odată ce ai verificat response.status === 'success', TypeScript îți garantează că data există și este de tipul corect. Nu mai ai nevoie de operatorul optional chaining (?.) sau de aserțiuni de tip forțate (as Data).

Trade-off-urile de care nu-ți spune nimeni

Să fim sinceri, niciun pattern nu e glonțul de argint. Am folosit abordarea asta pe zeci de rute de API și reduceri de Redux și am observat câteva chestii:

  • Merge brici pentru: Reducer actions în Redux/useReducer, stări de formulare complexe și răspunsuri de API unde structura se schimbă radical în funcție de succes sau eșec. Reduci bug-urile de UI la aproape zero.
  • E destul de nasol când: Ai un flux cu peste 10-15 stări intermediare. Codul devine destul de verbose, iar instrucțiunile switch încep să arate ca niște piramide egiptene. De asemenea, dacă folosești librării de UI care se așteaptă la flag-uri plate, va trebui să scrii funcții de mapare manuală, ceea ce înseamnă boilerplate suplimentar.

Pentru mine, reducerea bug-urilor cu cel puțin 30% pe partea de UI a meritat pe deplin cele câteva zeci de linii de cod în plus. Am eliminat complet acele ecrane blocate în "loading infinit" doar pentru că o variabilă nu a fost resetată corect pe un catch block.

Voi cum gestionați stările astea asincrone? Rămâneți la clasicele flag-uri booleene sau ați trecut deja pe unions?

Răspunsuri 0

Se încarcă răspunsurile…

Loghează-te pentru a răspunde

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