type FetchState<T> =
| { status: 'idle' }
| { status: 'loading' }
| { status: 'success'; data: T; updatedAt: Date }
| { status: 'error'; error: Error };
function renderUI<T>(state: FetchState<T>) {
switch (state.status) {
case 'idle':
return 'Așteaptă inițializarea...';
case 'loading':
return 'Se încarcă datele...';
case 'success':
// Compilatorul știe sigur că aici avem state.data și state.updatedAt
return `Date actualizate la ${state.updatedAt.toLocaleTimeString()}`;
case 'error':
// Aici avem acces doar la state.error, nu și la state.data
return `Eroare: ${state.error.message}`;
}
}Salutare. Vreau să vorbim despre discriminated unions în TypeScript, probabil cel mai util feature pe care mulți developeri îl ignoră sau îl folosesc doar pe jumătate. Vă arăt cum m-a salvat modelul ăsta de la zeci de bug-uri ciudate în producție și cum scriu acum stările pentru API-uri, reduceri sau mașini de stări simple.
Problema cu flag-urile booleene
Toți am trecut prin asta. Ai un ecran unde încarci niște date de pe un API și definești starea componentei cam așa:
{ isLoading: boolean, data?: Payload, error?: string }
E o abordare clasică, dar e o capcană masivă. Te trezești în situații absurde în care ai și isLoading: true, dar ai și data populat din rularea anterioară, plus o eroare rămasă de acum două minute. UI-ul nu mai știe ce să randeze. Ai creat, fără să vrei, stări imposibile din punct de vedere logic, dar perfect valide pentru compilator.
La un proiect mărișor, unde aveam vreo 14.000 de useri activi zilnic pe un dashboard financiar, aveam constant crash-uri în producție din cauza stărilor de loading și error prost gestionate. Codul era plin de if (data) { ... } else if (error) { ... } și mereu scăpam câte un caz de edge-case.
Cum rezolvă discriminated unions problema
Ideea e simplă: transformi starea dintr-un singur obiect cu proprietăți opționale într-o uniune de obiecte stricte, fiecare reprezentând o stare clară. Folosim un literal type comun (de obicei numit status sau type) care acționează ca un "discriminant".
TypeScript e destul de deștept încât, în momentul în care pui un switch sau un if pe acel câmp discriminant, îți oferă autocompletion și siguranță de tip doar pentru proprietățile specifice acelei stări.
Când folosești modelul ăsta într-un reducer din React sau Redux, compilatorul te obligă practic să tratezi corect fiecare caz. Dacă adaugi o stare nouă mai târziu, de exemplu status: 'reconnecting', TypeScript o să-ți dea eroare în build dacă ai uitat să o tratezi în switch.
Un trade-off sincer
Mai folosesc booleene? Foarte rar, doar pentru chestii extrem de izolate, gen dacă un dropdown e deschis sau închis. În rest, merg pe uniuni.
Totuși, să fim sinceri, există și un dezavantaj. Modelul ăsta devine destul de verbos când ai de-a face cu formulare mari sau când vrei să serializezi/deserializezi datele care vin din backend. Dacă API-ul tău nu trimite câmpul discriminator exact în formatul așteptat, trebuie să scrii custom type guards la intrare ca să validezi payload-ul. Mie mi-a luat ceva timp să-mi conving colegii din backend să structurăm răspunsurile unitar, dar după ce am făcut-o, am economisit cam 30% din timpul de debugging pe zona de integrare.
Voi cum gestionați stările astea asincrone în aplicațiile mari? Mergeți pe varianta clasică cu booleene sau ați trecut la uniuni discriminate?