eduardweb.
TypeScript avansatIntermediar#typescript#patterns#software-architecture

Cum m-au salvat discriminated unions de la bug-uri stupide în TypeScript

De Adrian Voicu, 22 mai 2026 · 7 vizualizări · 3 like-uri

Postat 22 mai 2026
typescript
type AsyncState<T> =
  | { status: 'idle' }
  | { status: 'loading' }
  | { status: 'success'; data: T; updatedAt: number }
  | { status: 'error'; error: Error };

function render(state: AsyncState<string[]>) {
  switch (state.status) {
    case 'idle': return 'Așteaptă...';
    case 'loading': return 'Se încarcă...';
    case 'success': return `Date: ${state.data.join(', ')} (Actualizat: ${state.updatedAt})`;
    case 'error': return `Eroare: ${state.error.message}`;
  }
}

Scriu TypeScript de prin 2016 și, dacă ar fi să aleg o singură chestie care mi-a salvat nopțile de debugging, aia e pattern-ul de discriminated unions. Dacă încă mai scrii interfețe cu zece câmpuri opționale doar ca să acoperi toate stările posibile ale unui API, te chinui singur.

Să-ți zic ce am pățit acum vreo trei ani la un proiect cu vreo 15.000 de utilizatori activi pe zi. Aveam un dashboard care trăgea date dintr-un API destul de instabil. Cum arăta tipul nostru de stare? Ceva de genul: { data?: UserData, loading: boolean, error?: string }.

Coșmarul boolean-urilor corelate

La prima vedere pare ok, nu? Dar în practică, ajunsesem să avem în componente verificări de tipul if (loading && !data && !error). Codul era plin de presupuneri fragile. Ce se întâmplă dacă loading e false, dar data e undefined și error e tot undefined? Aceea este o stare imposibilă în teorie, dar pe care sistemul nostru de tipuri o permitea fără probleme din cauza opționalelor.

Aici intervin discriminated unions (sau tagged unions). Ideea e simplă: creezi tipuri specifice pentru fiecare stare posibilă și le unești folosind o proprietate comună de tip literal, numită „discriminant”.

În loc de un singur obiect mare cu proprietăți opționale, spargi totul în stări atomice. TypeScript devine capabil să facă control flow analysis. Când verifici state.status === 'success', compilerul știe sigur că data există și că error nu are ce căuta acolo. Dispare complet nevoia de non-null assertions (!) sau de verificări redundante.

Unde strălucește pattern-ul ăsta?

Am început să folosesc pattern-ul ăsta peste tot și am redus bug-urile de UI cu cel puțin 40% în faza de dezvoltare. Îl folosesc în trei locuri mari și late:

  1. Răspunsuri de API: Când ai succes vs. eroare de validare vs. eroare de sistem. Fiecare are payload-ul lui specific.
  2. Reducer Actions: În Redux sau Zustand. Fiecare acțiune are un type și un payload specific acelui tip.
  3. State Machines simple: Când un widget poate fi în stări clare: idle, scanning, processing, completed.

Trade-off-uri: Nimic nu e gratis

Nu vreau să pară că asta e soluția magică pentru orice. Vine cu un cost.

Merge brici pentru stări clar definite și fluxuri liniare. În schimb, e destul de nasol când ai entități complexe care pot avea combinații dinamice de stări. Dacă ai 10 flag-uri care se pot combina în orice mod (de exemplu, un editor foto complex unde poți avea crop activ, filtre aplicate și export în același timp), numărul de uniuni crește exponențial. Riști să scrii un boilerplate imens doar ca să acoperi fiecare permutare. Pentru cazurile alea, e mai bine să rămâi pe flag-uri independente, chiar dacă pierzi din siguranța tipării stricte.

Voi cum gestionați stările asincrone în aplicațiile mari? Tot cu flag-uri booleene împrăștiate sau ați trecut pe uniuni stricte?

Răspunsuri 0

Se încarcă răspunsurile…

Loghează-te pentru a răspunde

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