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 read • Q2BSTUDIO Team

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.

A BREAK?

Play for a moment before you go

OUR SERVICES

How we can help you

Artificial intelligence

AI agents, chatbots, and intelligent assistants that automate tasks and serve your customers 24/7 to improve the efficiency of your business.

More info

Software Development

Web, mobile, and desktop applications, intranets, e-commerce, SaaS, and management platforms designed for your company's specific needs.

More info

Cloud services

Migration, infrastructure, managed hosting, high availability, and security on Microsoft Azure and Amazon Web Services to help your business scale without limits.

More info

Cybersecurity and pentesting

Security audits, penetration testing and protection of applications, data and infrastructure on-premise and cloud, with ethical hacking and regulatory compliance.

More info

Business Intelligence

Dashboards and data analysis with Power BI: we integrate your sources, design dashboards and KPIs and turn your data into decisions.

More info

Process automation

We automate repetitive tasks and connect your applications with n8n, Power Automate, Make, and RPA, eliminating manual work and increasing productivity.

More info

Training for Companies

We train your teams in technology with criteria: web development, databases, Git, best practices and security, automation with n8n, artificial intelligence for companies and creation of AI solutions with Azure AI Foundry.

More info

Code Auditing

We audit the code that you, your team or an AI create: we tell you what is good and what to improve, we secure it and make it ready for production, web or app.

More info

AI Image Generation

We create for you the images that your business needs with artificial intelligence: product, networks, advertising, illustration and avatars. You tell us what you want and we deliver it ready to use.

More info

AI Video Generation

We create videos with artificial intelligence for you: promotional, networking, virtual presenters, dubbing and animations. You tell us the idea and we will deliver it assembled and ready to publish.

More info

AI Conversational Avatars

We create conversational avatars with AI – digital humans with a face and voice – that serve your customers and teams with the knowledge of your company, on your website, interactive monitors, WhatsApp or Teams.

More info

Online Marketing and AI

Google Ads, Meta Ads, LinkedIn Ads and AI Engine Positioning (GEO/AEO): we attract customers and make your brand appear where they search for you, also on ChatGPT, Gemini and Perplexity.

More info

Do you have a project in mind?

Tell us your vision and we'll turn it into a software solution. Whatever the scope, we make your idea real.

Live Chat