Uniones discriminadas en TypeScript explicadas de forma simple

Descubre cómo las uniones discriminadas en TypeScript definen estados con un único discriminante, eliminando combinaciones imposibles y mejorando claridad, seguridad y pruebas.

21 sept 2025 • 6 min de lectura • Equipo Q2BSTUDIO

Inteligencia-Artificial-

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.

¿UNA PAUSA?

Juega un momento antes de irte

NUESTROS SERVICIOS

Cómo podemos ayudarte

Inteligencia artificial

Agentes de IA, chatbots y asistentes inteligentes que automatizan tareas y atienden a tus clientes 24/7 para mejorar la eficiencia de tu negocio.

Más info

Desarrollo de software

Aplicaciones web, móviles y de escritorio, intranets, e-commerce, SaaS y plataformas de gestión diseñadas para las necesidades concretas de tu empresa.

Más info

Servicios cloud

Migración, infraestructura, hosting gestionado, alta disponibilidad y seguridad en Microsoft Azure y Amazon Web Services para que tu negocio escale sin límites.

Más info

Ciberseguridad y pentesting

Auditorías de seguridad, test de intrusión (pentesting) y protección de aplicaciones, datos e infraestructura on-premise y cloud, con hacking ético y cumplimiento normativo.

Más info

Business Intelligence

Cuadros de mando y análisis de datos con Power BI: integramos tus fuentes, diseñamos dashboards y KPIs y convertimos tus datos en decisiones.

Más info

Automatización de procesos

Automatizamos tareas repetitivas y conectamos tus aplicaciones con n8n, Power Automate, Make y RPA, eliminando trabajo manual y aumentando la productividad.

Más info

Formación para empresas

Formamos a tus equipos en tecnología con criterio: desarrollo web, bases de datos, Git, buenas prácticas y seguridad, automatización con n8n, inteligencia artificial para empresas y creación de soluciones de IA con Azure AI Foundry.

Más info

Auditoría de código

Auditamos el código que creas tú, tu equipo o una IA: te decimos qué está bien y qué mejorar, lo securizamos y lo dejamos listo para producción, web o app.

Más info

Generación de imágenes con IA

Creamos por ti las imágenes que necesita tu negocio con inteligencia artificial: producto, redes, publicidad, ilustración y avatares. Tú nos dices qué quieres y te lo entregamos listo para usar.

Más info

Generación de vídeos con IA

Creamos por ti vídeos con inteligencia artificial: promocionales, para redes, presentadores virtuales, doblaje y animaciones. Nos cuentas la idea y te lo entregamos montado y listo para publicar.

Más info

Avatares conversacionales con IA

Creamos avatares conversacionales con IA —humanos digitales con cara y voz— que atienden a tus clientes y equipos con el conocimiento de tu empresa, en tu web, monitores interactivos, WhatsApp o Teams.

Más info

Marketing Online e IA

Google Ads, Meta Ads, LinkedIn Ads y posicionamiento en motores de IA (GEO/AEO): captamos clientes y hacemos que tu marca aparezca donde te buscan, también en ChatGPT, Gemini y Perplexity.

Más info

¿Tienes un proyecto en mente?

Cuéntanos tu visión y la convertimos en una solución de software. Sea cual sea el alcance, hacemos realidad tu idea.

Live Chat