C# por fin se mete en el terreno de los union types, y eso no es solo una novedad de lenguaje para mirar desde lejos. Si trabajas en APIs, backends de pagos, flujos de aprobación o integraciones empresariales, seguramente ya conoces el problema: un método puede devolver éxito, validación fallida, timeout, permiso denegado o un estado intermedio, y terminar modelando todo con object, bool, excepciones o clases demasiado genéricas.
La consecuencia suele ser la misma: código que compila, pero obliga a leer documentación, adivinar contratos o hacer if de más para descubrir qué salió mal. Con union types, la idea es expresar en el tipo mismo qué variantes son válidas. Eso hace que el compilador te ayude más y que el código sea más claro para quien lo mantiene después. La referencia técnica que detonó esta conversación es el artículo de Andrew Lock sobre el preview 2 de .NET 11, donde explora cómo llegan los union types a C# y qué forma toman en la práctica: https://andrewlock.net/exploring-the-dotnet-11-preview-2-dotnet-gets-union-types/
Qué son los union types y por qué te deberían importar
Un union type es un tipo que puede ser uno de varios tipos posibles, pero no todos a la vez. En vez de decir “esto devuelve algo”, defines “esto devuelve A o B o C”. En lenguajes funcionales esto existe hace años; en C# la propuesta apunta a llevar esa expresividad al mundo .NET sin obligarte a abandonar el estilo del ecosistema.
En la práctica, esto te sirve cuando un resultado tiene variantes reales y mutuamente excluyentes. Por ejemplo: una operación de pago puede terminar en PaymentApproved, PaymentDeclined o PaymentPending. Hoy muchas veces eso se modela con un enum más datos opcionales, o con una clase base y subclases, o con excepciones para todo lo que no sea éxito. Cada opción tiene costos. Un union type reduce la ambigüedad porque el contrato ya dice qué posibilidades existen.
La documentación y las discusiones de la propuesta en el ecosistema .NET apuntan a un objetivo claro: hacer más segura la representación de estados y resultados sin forzarte a escribir tanto código repetitivo. Si quieres seguir la base oficial del lenguaje, revisa la documentación de C# en Microsoft Learn: https://learn.microsoft.com/dotnet/csharp/
El problema real: contratos flojos
En proyectos enterprise, el problema no suele ser que falte una forma de devolver datos. El problema es que sobran formas de esconder errores. Un método que devuelve null a veces, una excepción otras veces y un DTO vacío en ciertos casos termina siendo una trampa para quien consume la API.
Mira este patrón, que seguro has visto más de una vez:
// Ejemplo conceptual, no idiomático para C#
function getCustomer(id: string): Customer | null | Error {
// ...
}
En C# actual, algo parecido suele resolverse con object, dynamic, una clase wrapper o excepciones. El resultado es el mismo: el consumidor tiene que entender demasiadas reglas implícitas. Con union types, el contrato puede decir exactamente qué variantes existen y el compilador puede obligarte a tratarlas.
Qué cambia para tu día a día
No vas a reescribir toda tu base de código mañana. Pero sí cambia cómo piensas ciertas piezas del sistema. En lugar de preguntar “¿cómo devuelvo esto sin romper nada?”, empiezas a preguntar “¿cuáles son los estados reales de este caso de uso?”. Esa pregunta sola mejora mucho el diseño.
Por ejemplo, en un flujo de onboarding podrías tener PendingVerification, Verified y Rejected. En vez de meter todo en una tabla con campos opcionales y banderas sueltas, puedes modelar cada estado con datos específicos. Eso reduce bugs por combinaciones inválidas, como un usuario “aprobado” que todavía trae motivo de rechazo.
Cómo se modelan hoy errores, estados y resultados en .NET
Antes de usar union types, conviene ver qué hacemos hoy y dónde se rompe la película. En C# y .NET, los patrones más comunes para representar variantes son excepciones, enum, herencia y wrappers como Result<T>. Todos sirven, pero no resuelven el mismo problema.
Las excepciones son útiles para fallos inesperados o no recuperables. No son tan buenas para errores de negocio que forman parte del flujo normal, como “saldo insuficiente” o “correo ya registrado”. Si usas excepciones para eso, tu código de control termina mezclado con tu lógica de dominio.
Los enum también ayudan, pero por sí solos no cargan datos. Un OrderStatus = Cancelled no te dice quién canceló ni cuándo. Ahí empiezan los campos opcionales, y con ellos los estados imposibles.
Comparación rápida de enfoques
| Enfoque | Ventaja | Problema típico | Mejor caso de uso |
|---|---|---|---|
| Excepciones | Separan el flujo feliz del error | Costosas como contrato de negocio | Fallos inesperados, infraestructura |
enum | Simple y legible | No carga datos por variante | Estados pequeños y cerrados |
| Herencia | Cada subclase puede tener datos propios | Más boilerplate y serialización más compleja | Dominios con jerarquías claras |
Result<T> | Explicita éxito o error | A veces se queda corto para más de 2 variantes | Operaciones con éxito/fallo |
| Union types | Expresan varias variantes excluyentes | Requieren aprender el nuevo patrón | Estados, errores y resultados con múltiples formas |
Lo que cambia con union types es que ya no tienes que forzar todo a una estructura binaria. Si un caso de uso tiene 3 o 4 resultados legítimos, el tipo puede reflejarlo sin inventar campos vacíos o semántica oculta.
Cuando un Result no alcanza
Result<T> funciona bien cuando tienes un éxito y un error. Pero en sistemas reales muchas operaciones no son binarias. Una solicitud puede quedar aprobada, rechazada por validación, en revisión manual o bloqueada por fraude. Forzar esos casos a un simple Ok o Error borra información.
Ese detalle importa en soporte, analítica y observabilidad. Si el sistema solo dice “falló”, tú después tienes que abrir logs o consultar otra tabla para saber qué pasó. Si el tipo ya distingue las variantes, el resto del sistema puede tomar decisiones más claras desde el principio.
Union types en C#: cómo se verían en un caso real
Supongamos un servicio de transferencias internas. Una transferencia puede ser aprobada, rechazada por fondos insuficientes o quedar pendiente por revisión antifraude. Con un union type, el contrato podría expresar esas tres variantes de forma directa.
La idea conceptual sería algo así:
// Pseudocódigo conceptual para ilustrar la forma del contrato
TransferResult = Approved | InsufficientFunds | UnderReview
En C# actual, muchos equipos modelan esto con una clase base y tres derivadas, o con un record y propiedades opcionales. Con union types, el consumidor puede hacer pattern matching y cubrir cada caso de forma explícita. Eso reduce el riesgo de olvidar una variante nueva cuando alguien extienda el dominio.
Ejemplo de lectura con pattern matching
var result = transferService.CreateTransfer(request);
return result switch
{
TransferApproved approved => Results.Ok(new { approved.TransferId }),
TransferInsufficientFunds funds => Results.BadRequest(new { funds.Balance, funds.Required }),
TransferUnderReview review => Results.Accepted(new { review.ReviewId }),
_ => Results.StatusCode(500)
};
Ese switch no es solo sintaxis bonita. Te obliga a pensar en cada variante y deja muy claro qué respuesta HTTP corresponde a cada una. En APIs REST eso se traduce en contratos más consistentes y menos improvisación entre equipos.
Un caso de uso en apps empresariales
Imagina una plataforma de compras corporativas. Un pedido puede estar Draft, Submitted, Approved, Rejected o Expired. Hoy muchos sistemas guardan eso en una sola tabla con columnas opcionales, y luego cada pantalla decide qué campos leer.
Con union types, puedes separar mejor las variantes y darles forma propia. Rejected puede incluir reason y rejectedBy. Approved puede incluir approvedAt y approverId. Draft quizá ni siquiera tenga un approverId. Ese diseño hace más difícil crear combinaciones inválidas.
Beneficios concretos para equipos .NET
La primera ganancia es obvia: el código se vuelve más expresivo. Pero hay beneficios más prácticos que se sienten en equipos medianos y grandes, sobre todo cuando varias personas tocan el mismo dominio y no todas conocen el contexto de negocio.
El segundo beneficio es la seguridad. Si el compilador sabe que existen tres variantes, puede ayudarte a detectar cuando una rama quedó sin manejar. Eso no elimina bugs, pero sí quita una clase entera de errores por omisión.
El tercer beneficio es la mantenibilidad. Cuando un nuevo dev entra al proyecto, leer un tipo de unión bien nombrado es más fácil que descifrar una combinación de bool, string? y object?. En equipos distribuidos, eso ahorra tiempo real.
Donde más se nota
- APIs con respuestas de negocio complejas, no solo
200o500. - Flujos de aprobación con estados intermedios.
- Integraciones con terceros donde la respuesta puede venir en varios formatos válidos.
- Procesos batch que necesitan reportar éxito parcial, error recuperable o reintento.
- Dominios con reglas estrictas, como pagos, seguros, salud o logística.
Si trabajas en una fintech en Ecuador, Colombia, México o Perú, probablemente ya has visto sistemas donde un mismo endpoint devuelve mensajes distintos según el caso. Union types te ayudan a ordenar ese caos sin meter una capa extra de magia.
Qué mejora en testing
Cuando el tipo expresa las variantes, tus tests pueden enfocarse en casos reales, no en adivinar combinaciones. En vez de escribir pruebas para comprobar que un null significa rechazo, pruebas que el resultado sea exactamente InsufficientFunds.
Eso también mejora el nombre de los tests. Un test como should_return_insufficient_funds_when_balance_is_low es mucho más claro que should_return_false. La intención queda escrita en el contrato y en la prueba.
Cómo adoptarlos sin romper tu base actual
No necesitas migrar todo de golpe. De hecho, si lo haces sin criterio, solo cambias un problema por otro. Lo más sensato es empezar donde el dominio ya está pidiendo una representación más rica: resultados de negocio, workflows y estados que hoy viven repartidos entre enum, null y excepciones.
Si tu solución ya usa Result<T> o OneOf-style wrappers, el salto conceptual será menor. El objetivo no es meter una moda nueva, sino reducir fricción y hacer más difícil escribir código incorrecto.
Plan práctico de adopción
- Identifica 3 o 4 métodos donde hoy haya demasiados
ifo demasiadosnull. - Enumera las variantes reales del caso de uso, no las variantes técnicas.
- Define nombres que suenen a negocio, no a implementación.
- Usa pattern matching para obligarte a manejar todos los casos.
- Mantén excepciones solo para fallos inesperados o infra.
- Revisa serialización y contratos HTTP si el tipo cruza la frontera de una API.
Qué revisar antes de meterlos en producción
No todo tipo bonito es buen contrato externo. Si expones estos tipos en una API pública, piensa en compatibilidad, serialización y versionado. Un cambio en variantes puede afectar clientes móviles, otros servicios o integraciones de terceros.
También revisa herramientas de documentación y generación de clientes. Si tu stack depende de OpenAPI, prueba cómo se documentan las variantes. A veces el beneficio del tipo en código no se refleja automáticamente en el contrato HTTP, y ahí necesitas una capa adicional de claridad.
Limitaciones y puntos a vigilar
Union types no son una excusa para modelar mal. Si el dominio tiene 20 estados posibles porque el proceso está mal definido, el problema no se arregla con sintaxis nueva. Primero ordena el negocio, después el lenguaje.
Tampoco conviene abusar de ellos en capas donde el tipo no aporta mucho. Un DTO simple para listar usuarios no necesita una unión solo porque se puede. Si el dato es estable y plano, mantén el modelo simple.
Otra precaución: el equipo tiene que entender el patrón. Si solo una persona lo usa y el resto no sabe leerlo, puedes terminar con código elegante pero inconsistente. Vale la pena acordar convenciones de nombres, manejo de errores y uso de pattern matching.
Reglas sanas para no pasarte de listo
- Usa union types cuando existan variantes mutuamente excluyentes y relevantes para el negocio.
- No los uses para esconder falta de diseño en el dominio.
- Prefiere nombres concretos:
PaymentApproved, noVariant1. - Mantén la serialización simple si el tipo cruza APIs.
- Documenta qué variante corresponde a cada estado HTTP o evento.
Si quieres ampliar el contexto sobre diseño de APIs y contratos, también te puede servir revisar buenas prácticas de versionado en endpoints, por ejemplo en nuestro contenido sobre /blog/versionado-de-apis-en-dotnet.
Tabla resumen
| Pregunta | Respuesta corta |
|---|---|
| ¿Qué resuelven los union types? | Variantes excluyentes de un mismo resultado o estado. |
| ¿Reemplazan a las excepciones? | No, solo para errores de negocio o estados esperados. |
| ¿Sirven para APIs? | Sí, si el contrato necesita varias respuestas válidas. |
| ¿Mejoran el testing? | Sí, porque los casos quedan más explícitos. |
| ¿Son para todo el código? | No, solo donde el dominio tenga variantes reales. |
| ¿Ayudan en equipos grandes? | Sí, porque hacen el contrato más claro y mantenible. |
Los union types no van a arreglar por sí solos un sistema mal diseñado, pero sí te dan una herramienta mejor para modelar lo que ya existe en negocio real. Si hoy tu código depende de null, bool y excepciones para representar estados distintos, vale la pena mirar esta novedad con calma. En aplicaciones empresariales, esa claridad se traduce en menos bugs, menos suposiciones y menos tiempo perdido leyendo código ajeno.
Si estás construyendo servicios en .NET y quieres que el compilador te ayude más, este es uno de esos cambios que sí merecen atención. No porque suene moderno, sino porque encaja con problemas que ya resuelves todos los días.
Preguntas frecuentes
¿Qué es un union type en C#?
¿Union types reemplaza a Result<T>?
¿Conviene usar union types para errores de validación?
¿Esto ayuda en APIs REST?
¿Es buena idea usar union types en todos los DTOs?
¿Qué ventaja tienen frente a una clase base con herencia?
¿Sirven para equipos en LatAm que mantienen sistemas legacy?
Azirgo
¿Listo para construir tu Producto Digital?
Sitios web, apps móviles, software a medida y soluciones blockchain. Cuéntanos qué tienes en mente y armamos un plan claro contigo.
- Cotización clara en 48 horas
- Equipo en Ecuador, atención en español
- Desde un MVP hasta un producto en producción