type RemoteData<T> =
| { status: 'idle' }
| { status: 'loading' }
| { status: 'success'; data: T }
| { status: 'error'; error: string };
function renderContent(state: RemoteData<string[]>) {
switch (state.status) {
case 'idle':
return 'Apasă pentru a căuta';
case 'loading':
return 'Se încarcă lista...';
case 'success':
return `Rezultate: ${state.data.join(', ')}`;
case 'error':
return `A apărut o problemă: ${state.error}`;
default: {
const _exhaustiveCheck: never = state;
return _exhaustiveCheck;
}
}
}Dacă încă ai interfețe de state pline de câmpuri opționale de genul isLoading: boolean, data?: User și error?: Error, construiești bombe cu ceas. Am pățit-o acum vreo trei ani pe un proiect cu 14k utilizatori activi zilnic: un checkout flow a crăpat în producție pentru că isLoading devenise false, dar data era undefined iar error era null. Niciun if din cod nu prinsese cazul ăsta bizar, iar utilizatorii vedeau doar un ecran alb cu spinner-ul blocat.
TypeScript ne lasă să modelăm stările imposibile dacă îl lăsăm. Soluția pe care o folosesc obsesiv de atunci sunt discriminated unions (sau tagged unions). Ideea e banală: folosești un câmp comun literal — de obicei status sau type — care îi spune compilatorului exact ce alte proprietăți există în acel moment.
Unde strălucește pattern-ul
Îl folosesc în trei locuri mari și late:
-
API Responses și UI State: În loc să ghicești dacă ai date sau eroare, forțezi starea să fie una singură. Dacă
status === 'success', ai garantatdata. Compilatorul nu te lasă să accesezistate.datacândstatus === 'loading', punct. Nu mai ai nevoie dedata?.user?.nameîmprăștiat peste tot. -
Acțiuni de Reducer (Redux / useReducer): În loc de un generic
action.payloadcu tipulanysau un union lălăit, fiecare acțiune are tipul ei fix. Când faci switch peaction.type, payload-ul se îngustează automat. -
State machines simple: Când ai pași complecși de onboarding sau wizard-uri, fiecare pas are propriul set de validări și câmpuri. Nu ții tot formularul într-un singur obiect masiv plin de proprietăți opționale.
Trucul cu never pentru exhaustivitate
Partea cea mai mișto nu e că te ajută la scriere, ci la refactoring. Când adaugi o stare nouă (de exemplu, 'stale' sau 'revalidating'), vrei ca tot proiectul să urle la tine dacă ai uitat să o tratezi undeva.
Pentru asta folosesc mereu un bloc default cu assignare la tipul never. Dacă ai acoperit toate cazurile posibile în switch, TypeScript e fericit. În secunda în care ai adăugat un nou status în union și ai uitat un case, primești eroare de compilare direct la linia respectivă, nu surprize la QA.
Care e compromisul sincer?
Nu e totul lapte și miere. Trade-off-ul principal e că scrii ceva mai mult cod boilerplate la început. Trebuie să definești tipuri separate pentru fiecare stare în loc să trântești o interfață cu trei boolean-uri.
Al doilea minus apare la integrarea cu librării externe sau formulare dinamice (cum ar fi React Hook Form), unde structurile plate și mutabile sunt adesea preferate de API-urile lor. Să transformi payload-ul brut venit din backend într-un discriminated union curat cere un pas intermediar de parsare — eu folosesc Zod pentru asta la granița aplicației.
Merită efortul? Fără discuție. Câștigi certitudine matematică în UI pentru un cost minor de tastare.
Voi cum gestionați stările de fetch în frontend? Vă bazați pe unelte ca TanStack Query sau încă aveți propriile handlere scrise manual?