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 'loading':
return 'Se încarcă...';
case 'error':
// TypeScript știe sigur că aici există 'error'
return `Eroare: ${state.error.message}`;
case 'success':
// Aici avem acces garantat la 'data' și 'updatedAt'
return `Date actualizate la ${state.updatedAt.toLocaleTimeString()}`;
default:
return 'Pregătit pentru descărcare';
}
}Am trecut recent un proiect cu vreo 12.000 de utilizatori activi de la clasicele interfețe pline de câmpuri opționale la pattern-ul de discriminated unions. A fost probabil cea mai bună decizie de refactoring din ultimul an. Am eliminat complet acele verificări enervante de tip if (response.data && !response.error) care umpleau codebase-ul de zgomot și făceau mentenanța un calvar.
În TypeScript, mulți developeri tind să definească stările unui API sau ale unui reducer folosind o singură interfață mare și lată, unde jumătate din proprietăți sunt opționale. Este o capcană clasică. Compilatorul nu te poate ajuta pentru că, din punctul lui de vedere, orice combinație de proprietăți este validă, chiar și cele imposibile în realitate.
Problema cu stările „opționale”
Să luăm exemplul clasic al unui request HTTP. Dacă ai o interfață cu loading: boolean, data?: T și error?: string, ai o problemă de design. Ce se întâmplă când loading e true, dar ai și data și error populate în același timp? Este o stare invalidă, dar complet posibilă conform tipurilor tale.
Când am refăcut codul pentru clienții noștri, am observat că echipa pierdea ore bune în QA testând scenarii care pur și simplu nu ar fi trebuit să existe la nivel de compilare. Soluția a fost să forțăm compilatorul să înțeleagă că stările sunt disjuncte.
Cum rezolvă discriminated unions problema
Ideea din spate e simplă: folosești o proprietate comună (un „literal type” cum ar fi status sau type) care servește drept discriminator. TypeScript este destul de deștept încât, odată ce ai verificat acea proprietate într-un if sau switch, să știe exact ce alte câmpuri sunt disponibile în acel bloc de cod.
Această abordare strălucește în trei locuri: în răspunsurile de la API, în acțiunile din Reducer (sau orice alt state manager) și în mașinile de stare simple pe care le scrii pentru UI.
Trade-off-ul sincer: unde doare?
Nimic nu e perfect pe lumea asta și vreau să fiu foarte sincer cu voi. Pattern-ul ăsta vine la pachet cu un pic de boilerplate.
În primul rând, destructurarea devine enervantă. Nu poți să faci direct const { data, error } = state pentru că TypeScript o să urle că data nu există în starea de loading. Trebuie să verifici mai întâi discriminatorul și abia apoi să destructurezi în interiorul blocului de decizie.
În al doilea rând, dacă ai stări foarte imbricate, tipurile pot deveni greu de citit pentru un junior din echipă. Totuși, am economisit cam 30% din timpul de debugging în producție de când am adoptat abordarea asta pe scară largă, deci merită cu vârf și îndesat.
Voi cum gestionați stările complexe în aplicațiile voastre? Mergeți pe varianta clasică cu proprietăți opționale sau ați trecut deja la discriminated unions?