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

De ce folosesc Discriminated Unions în TypeScript pentru orice stare complexă

De Marian Apostol, 2 aug. 2026 · 5 vizualizări · 3 like-uri

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

function renderUserProfile(state: AsyncState<{ name: string; email: string }>) {
  switch (state.status) {
    case 'idle':
      return 'Apasă pe buton pentru a încărca datele.';
    case 'loading':
      return 'Se încarcă profilul...';
    case 'success':
      // TypeScript știe sigur că state.data există aici!
      return `Salut, ${state.data.name}! (${state.data.email})`;
    case 'error':
      // TypeScript știe sigur că state.error există aici!
      return `A apărut o eroare: ${state.error.message}`;
  }
}

Am refăcut acum vreo jumătate de an modulul de checkout la un SaaS cu peste 12k utilizatori activi lunar. Înainte aveam vreo 5 flag-uri booleane aruncate într-un singur hook pentru starea UI-ului și, evident, ajungeam des în stări bizaro-imposibile: isLoading era true, dar aveam deja error populat și data veche afișată în spate. Trecerea la discriminated unions a eliminat din start aproape 90% din bug-urile alea enervante din interfață.

Ce este mai exact un Discriminated Union?

Conceptul e simplu: o uniune de tipuri de obiecte care au toate o proprietate comună literală — un așa-numit "discriminator" sau "tag" (de obicei numit status, type sau kind).

Datorită acestui câmp comun, sistemul de tipuri din TypeScript este capabil să facă narrowing automat. Când verifici valoarea acelei proprietăți într-un if sau switch, TypeScript știe exact ce alte proprietăți există pe acel obiect în acel ramificator de cod și ce proprietăți nu au ce căuta acolo.

Unde aplic pattern-ul ăsta în producție

Îl folosesc intensiv în trei scenarii principale de care mă lovesc zilnic:

  1. Stările de UI / Async Data: În loc de un obiect gigantic cu isFetching, isSuccess, isError, data și error, definesc stări explicite: idle, loading, success și error. În starea de loading, e imposibil din punct de vedere al tipurilor să accesezi data sau error fără să verifici statusul.
  2. Răspunsuri de la API: Când creez wrapper-e peste fetch-uri, returnez fie { success: true, payload: T }, fie { success: false, error: ApiError }. Nu mai există situația în care cineva din echipă uită să verifice dacă request-ul a eșuat și încearcă să citească direct răspunsul.
  3. Redux / Reducer Actions: Este pattern-ul clasic din Redux ecosystem, unde fiecare acțiune are un type unic și un payload specific tipului respectiv.

Trade-off-uri reale (unde devine enervant)

Ca orice pattern, vine la pachet și cu dezavantaje, iar cine spune că e glonțul de argint minte.

În primul rând, scrii ceva mai mult boilerplate când instanțiezi obiectele. Nu mai poți face un simplu update parțial pe stare cu un spread operator ...state fără să fii foarte atent ce status setezi, altfel tipurile nu se potrivesc.

În al doilea rând, când ai nevoie doar de o proprietate simplă adânc îngropată într-o stare de success și vrei să o pasezi rapid unei componente copil, ești obligat să faci destrucțurare sau verificare înainte. Nu e la fel de comod ca un obiect plat la prototipare rapidă sau când scrii un script aruncat în 10 minute.

Cu toate astea, la proiecte mari cu echipe de 5+ devi care ating aceeași bază de cod, siguranța pe care ți-o oferă la refactoring merită fiecare linie de cod în plus.

Voi cum gestionați stările imposibile în aplicațiile de React sau Node? Mai aveți componente bătrânești cu 6 booleene la grămadă?

Răspunsuri 0

Se încarcă răspunsurile…

Loghează-te pentru a răspunde

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