Las uniones discriminadas en TypeScript resuelven un problema común en aplicaciones reales: cuando el estado se describe con varias banderas sueltas se crean combinaciones imposibles y confusas. Por ejemplo variables como let isLoading = false; let data: User | null = null; let error: string | null = null; dejan abierta la pregunta que ocurre si isLoading es true y error tiene valor al mismo tiempo. Las uniones discriminadas obligan a que la aplicacion esté en exactamente un estado y que cada estado lleve solo los datos que necesita.

Un ejemplo de tipo para peticiones es type FetchState<T> = | { kind: idle } | { kind: loading } | { kind: success; data: T } | { kind: error; message: string } El campo compartido kind actua como discriminante y los demas campos pertenecen solo a cada variante, de modo que combinaciones imposibles como loading y error al mismo tiempo dejan de existir.

Beneficios claves: claridad porque al leer kind se sabe inmediatamente lo que pasa; seguridad porque TypeScript reduce el tipo internamente y garantiza que en success exista data; guiado por el compilador porque al añadir un nuevo estado el compilador señala todos los lugares que hay que actualizar.

Como usarlo Paso 1 definir los estados elige un unico nombre para la etiqueta, por ejemplo kind o type type AuthState = | { kind: loggedOut } | { kind: loggingIn } | { kind: loggedIn; user: { id: string; name: string } } | { kind: loginFailed; message: string }

Paso 2 escribir el codigo que discrimina sobre la etiqueta function renderAuth(s: AuthState): string { switch(s.kind) { case loggedOut: return Por favor inicia sesion; case loggingIn: return Iniciando sesion; case loggedIn: return Bienvenido espacio + s.user.name; case loginFailed: return Error espacio + s.message; default: return assertNever(s); } } function assertNever(x: never): never { throw new Error(); }

Paso 3 actualizar estado con funciones pequeñas y puras function startLogin(_s: AuthState): AuthState { return { kind: loggingIn }; } function loginOk(_s: AuthState, user: { id: string; name: string }): AuthState { return { kind: loggedIn, user }; } function loginFail(_s: AuthState, message: string): AuthState { return { kind: loginFailed, message }; } Mantener estas funciones puras hace que las pruebas sean sencillas y estables.

Patrones cotidianos A) Peticiones asincronas sin isLoading type FetchState<T> = | { kind: idle } | { kind: loading } | { kind: success; data: T } | { kind: error; message: string; retryAfterMs?: number } Uso en UI function renderUsers(s: FetchState<User[]>) { switch(s.kind) { case idle: return Haz clic para cargar; case loading: return Cargando; case success: return Cargados espacio + s.data.length + espacio usuarios; case error: return Error espacio + s.message; default: return assertNever(s); } }

B) Formularios como pequeños flujos type ProfileForm = | { kind: editing; values: { name: string; email: string }; errors?: Record<string, string> } | { kind: submitting; values: { name: string; email: string } } | { kind: submitted; id: string } | { kind: failed; values: { name: string; email: string }; message: string } Con esto ya no necesitas campos dispersos como isSubmitting o submitError, el propio estado lo dice todo.

C) Flags de despliegue que no se pueden usar mal type Rollout = | { kind: off } | { kind: percentage; percent: number } | { kind: audience; segments: Array<beta | staff | pro> } | { kind: on } function isEnabled(flag: Rollout, ctx: { segment: string; rand: number }) { switch(flag.kind) { case off: return false; case on: return true; case percentage: return ctx.rand < flag.percent / 100; case audience: return flag.segments.includes(ctx.segment as any); default: return assertNever(flag); } }

D) Resultados y opciones para evitar try catch por todas partes type Result<T, E> = { kind: ok; value: T } | { kind: err; error: E } type Option<T> = { kind: some; value: T } | { kind: none } const ok = <T, E = never>(value: T): Result<T, E> => ({ kind: ok, value }); const err = <E, T = never>(error: E): Result<T, E> => ({ kind: err, error }); const some = <T>(value: T): Option<T> => ({ kind: some, value }); const none = <T = never>(): Option<T> => ({ kind: none }); function parseJson<T>(s: string): Result<T, string> { try { return ok(JSON.parse(s) as T); } catch (e) { return err(Invalid JSON); } }

Anejos utiles Pequeñas guardas de tipo para legibilidad const isSuccess = <T>(s: FetchState<T>): s is { kind: success; data: T } => s.kind === success; if (isSuccess(state)) { // state.data disponible y tipado }

Un matcher compacto para mapeos en una linea function match<T extends { kind: string }, R>(v: T, handlers: { [K in T[ kind ]]: (x: Extract<T, { kind: K }>) => R }): R { return handlers[v.kind](v as any); } Ejemplo de uso const label = match(state, { idle: () => Idle, loading: () => Cargando, success: s => Got espacio + s.data.length, error: s => Error espacio + s.message });

Migracion y buenas practicas Lista las banderas actuales que describen modos como isLoading, hasError, status. Nombra los estados que cubren la realidad por ejemplo idle | loading | success | error. Mueve campos dentro de su estado correspondiente. Reemplaza if anidados por switch sobre state.kind. Añade assertNever para detectar casos no manejados. Hazlo modulo a modulo y veras beneficios inmediatos.

Pruebas facilitan porque las funciones que cambian estado son puras import { it, expect } from vitest it(Va loading a success, () => { const s0: FetchState<User> = { kind: idle }; const s1: FetchState<User> = { kind: loading }; const s2: FetchState<User> = { kind: success, data: { id: 1, name: Soumaya } }; expect(s0.kind).toBe(idle); expect(s1.kind).toBe(loading); expect(s2.kind).toBe(success); }); No necesitas simular timers o red.

Errorres comunes usar distintos nombres para la etiqueta hace que no funcione el estrechamiento de tipos; campos opcionales en exceso cuando un campo solo importa en una situacion concreta en lugar de crear una variante; demasiados estados minimos que actuan igual y contienen los mismos datos; olvidar exhaustividad si no se preserva assertNever.

Reflexion final Las uniones discriminadas son simples pero potentes, nombran tus estados, hacen imposibles los estados incoherentes, mejoran la claridad y el autocompletado y convierten los cambios en algo seguro gracias al compilador. Si tienes ahora un lugar con isLoading mas error mas data comienza por ahi y convertelo a una union con kind, notaras la diferencia de inmediato.

En Q2BSTUDIO aplicamos estas y otras buenas practicas para desarrollar aplicaciones a medida y software a medida robustas y mantenibles. Somos especialistas en inteligencia artificial, ciberseguridad y servicios cloud aws y azure y ofrecemos servicios de inteligencia de negocio, ia para empresas, agentes IA y soluciones con power bi. Si te interesa mejorar la calidad del codigo, arquitectura y pruebas para tus proyectos a medida contacta con nosotros y descubre como podemos ayudar a tu empresa.