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

Discriminated unions: Cum scapi de if-uri inutile în TypeScript

De Alexandru Matei, 17 iul. 2026 · 14 vizualizări · 2 like-uri

Postat 17 iul. 2026
typescript
type PaymentState =
  | { status: 'idle' }
  | { status: 'loading' }
  | { status: 'success'; transactionId: string; amount: number }
  | { status: 'error'; error: string; code: number };

function handlePayment(state: PaymentState) {
  switch (state.status) {
    case 'idle':
      return 'Inițiază plata';
    case 'loading':
      return 'Se procesează...';
    case 'success':
      // TypeScript știe sigur că transactionId există și e string
      return `Succes! Tranzacția: ${state.transactionId.toUpperCase()}`;
    case 'error':
      // Aici avem acces garantat la error și code
      return `Eroare [${state.code}]: ${state.error}`;
  }
}

Am văzut prea des cod TypeScript plin de assert-uri dubioase sau verificări opționale obositoare doar pentru că structura de date e gândită strâmb. Discriminated unions e cel mai simplu mod de a forța compilatorul să lucreze pentru tine, nu împotriva ta. După ce am aplicat pattern-ul ăsta pe un flow de checkout, am redus bug-urile de UI cu vreo 30% și am șters zeci de rânduri de cod defensiv.

Problema stărilor „grubby”

La un proiect trecut, o platformă cu vreo 12k de utilizatori activi, m-am lovit de o structură clasică de state pentru plăți. Arăta cam așa:

interface PaymentState { status: 'idle' | 'loading' | 'success' | 'error'; transactionId?: string; error?: string; }

La prima vedere, pare ok. Dar la a doua privire, e o bombă cu ceas. Ce te oprește să ai status: 'success' dar transactionId să fie undefined din greșeală? Nimic. Compilatorul nu te ajută. Trebuia să scriem if (state.status === 'success' && state.transactionId) peste tot prin componente. O prostie care consumă timp și aduce erori logice.

Soluția: Discriminantul comun

Ideea din spatele discriminated unions (sau tagged unions) este să împarți starea în tipuri distincte, care au un singur câmp comun literal (discriminantul). În cazul nostru, câmpul status.

Când TypeScript vede un bloc switch sau if bazat pe acel discriminant, reduce automat tipul obiectului la varianta corectă. Nu mai ai nevoie de casting, nu mai ai nevoie de operatorul ? aruncat la întâmplare.

Pe lângă reducer actions și state machines, folosesc asta masiv pentru răspunsurile de la API. Decât să returnez un obiect generic cu { data?: T; error?: string }, prefer să separ complet succesul de eșec. Codul devine incredibil de curat și predictibil.

Trade-off-ul sincer

Nimic nu e perfect pe lumea asta. Pattern-ul funcționează genial în interiorul aplicației tale, dar are o mare vulnerabilitate: granița cu exteriorul.

TypeScript există doar la build time. Dacă API-ul tău de backend trimite o structură dubioasă care nu respectă contractul, te vei trezi cu crash-uri la runtime, deși tipurile tale compilau perfect. Pentru asta, ai nevoie de o validare suplimentară la intrare (eu folosesc Zod pentru asta, merită fiecare kilobait în plus).

Un alt dezavantaj minor e boilerplate-ul. Dacă ai 10 stări ușor diferite, trebuie să scrii 10 tipuri separate. Însă, sincer să fiu, prefer să scriu 20 de linii de tipuri în plus decât să caut un bug de stare în producție la ora 10 seara.

Voi folosiți pattern-ul ăsta des sau preferați să mergeți pe interfețe clasice cu proprietăți opționale și validări manuale?

Răspunsuri 0

Se încarcă răspunsurile…

Loghează-te pentru a răspunde

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