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

De ce nu mai scriu cod fără Discriminated Unions în TypeScript

De Mihai Popescu, 3 iun. 2026 · 2 vizualizări · 2 like-uri

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

function renderUI<T>(state: ApiResponse<T>) {
  switch (state.status) {
    case 'idle':
      return 'Inițializare...';
    case 'loading':
      return 'Se încarcă datele...';
    case 'success':
      // TypeScript știe exact că state are 'data' și 'updatedAt' aici
      return `Date primite la ${state.updatedAt}: ${JSON.stringify(state.data)}`;
    case 'error':
      // TypeScript știe sigur că state are proprietatea 'error'
      return `Eroare: ${state.error.message}`;
  }
}

Am trecut acum doi ani un proiect destul de mărișor (pe la 14k useri activi) de la stări împrăștiate cu booleeni la discriminated unions în TypeScript. A fost momentul în care am realizat cât de mult timp pierdeam cu bug-uri stupide de UI. Dacă ai avut vreodată ecrane care arătau și spinner-ul de loading și eroarea în același timp, știi exact despre ce vorbesc.

Problema stărilor imposibile

Clasicul mod în care văd definită starea unui apel API în multe proiecte arată cam așa: loading: boolean, error?: string, data?: Payload. La prima vedere, pare ok. Dar dacă analizezi puțin, tipul ăsta de structură permite stări absurde. Poți avea, teoretic, loading: true și data populat și error prezent în același timp.

Compilerul de TypeScript nu are cum să te ajute aici pentru că i-ai spus că toate sunt opționale. Ajungi să scrii în componentă chestii de genul if (loading && !data && !error) și deja codul devine greu de urmărit.

Aici intervin discriminated unions (sau tagged unions). Ideea e simplă: folosești un literal type unic (de obicei numit type, kind sau status) pe post de discriminant, care îi spune compilatorului exact în ce ramură a uniunii te afli.

Cum arată în practică

Uită-te la exemplul de cod atașat. Am definit tipul ApiResponse<T> ca o uniune de trei stări complet distincte.

Când facem pattern matching (cu un simplu switch în TypeScript), compilatorul e extrem de deștept. În interiorul blocului case 'success', el știe sigur că data există și că error nu are ce căuta acolo. Nu mai ai nevoie de type casting-uri dubioase sau de operatorul !. Dacă încerci să accesezi state.data în ramura de error, TypeScript îți dă instant peste degete la build.

Unde folosesc pattern-ul ăsta zi de zi

  1. Reducer Actions (Redux / useReducer): În loc de un tip generic Action cu un payload opțional de tip any, folosesc o uniune de acțiuni stricte. Fiecare acțiune are tipul ei și payload-ul ei specific.
  2. State Machines simple: Când am de implementat fluxuri complexe de checkout sau onboarding. Starea poate fi Draft, PendingPayment, Completed sau Failed. Fiecare stare are nevoie de alte date atașate (de exemplu, doar în Failed ai nevoie de un reason).
  3. Formulare dinamice: Când câmpurile afișate depind direct de o selecție anterioară (de exemplu, tipul de persoană: fizică sau juridică).

Trade-off-uri sincere

Nimic nu e perfect pe lumea asta, așa că hai să vedem unde scârțâie modelul ăsta.

Merge excelent pentru frontend și pentru modelat răspunsuri curate de API. În schimb, devine destul de greoi când ai stări foarte imbricate și vrei să faci update-uri parțiale fără o librărie helper gen immer. TypeScript te obligă să reconstruiești tot obiectul ca să respecți structura uniunii.

Un alt dezavantaj este că depinzi direct de backend. Dacă API-ul tău returnează doar { data: ... } sau { error: ... } fără un câmp explicit care să acționeze ca discriminant (cum ar fi un status code sau un type literal), trebuie să îți scrii propriile funcții de type guard în frontend ca să mapezi datele în uniune. Asta înseamnă boilerplate în plus.

La dashboard-ul de care ziceam, după ce am refăcut tot state management-ul pe baza acestui pattern, am eliminat complet o clasă întreagă de bug-uri legate de tranziții de stare invalide. Am economisit cam 20% din timpul de debugging pe partea de UI.

Voi cum gestionați stările asincrone în aplicații? Mergeți pe varianta clasică cu booleeni separați sau ați adoptat deja discriminated unions?

Răspunsuri 0

Se încarcă răspunsurile…

Loghează-te pentru a răspunde

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