Una persona desarrolladora revisa código C# en una pantalla de oficina mientras al lado hay tarjetas físicas que representan estados como éxito, error y pendiente.
Volver al blog

C# suma union types: qué cambia para .NET

C# suma union types y eso cambia cómo modelas errores, estados y resultados en aplicaciones .NET. Aquí ves cuándo usarlos, qué ventajas traen para equipos enterprise y ejemplos prácticos para devs en LatAm.

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

EnfoqueVentajaProblema típicoMejor caso de uso
ExcepcionesSeparan el flujo feliz del errorCostosas como contrato de negocioFallos inesperados, infraestructura
enumSimple y legibleNo carga datos por varianteEstados pequeños y cerrados
HerenciaCada subclase puede tener datos propiosMás boilerplate y serialización más complejaDominios con jerarquías claras
Result<T>Explicita éxito o errorA veces se queda corto para más de 2 variantesOperaciones con éxito/fallo
Union typesExpresan varias variantes excluyentesRequieren aprender el nuevo patrónEstados, 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

  1. APIs con respuestas de negocio complejas, no solo 200 o 500.
  2. Flujos de aprobación con estados intermedios.
  3. Integraciones con terceros donde la respuesta puede venir en varios formatos válidos.
  4. Procesos batch que necesitan reportar éxito parcial, error recuperable o reintento.
  5. 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

  1. Identifica 3 o 4 métodos donde hoy haya demasiados if o demasiados null.
  2. Enumera las variantes reales del caso de uso, no las variantes técnicas.
  3. Define nombres que suenen a negocio, no a implementación.
  4. Usa pattern matching para obligarte a manejar todos los casos.
  5. Mantén excepciones solo para fallos inesperados o infra.
  6. 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, no Variant1.
  • 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

PreguntaRespuesta 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#?
Es un tipo que puede representar una de varias variantes posibles, pero no todas al mismo tiempo. Sirve para modelar estados, errores o resultados cuando el dominio tiene opciones excluyentes y bien definidas.
¿Union types reemplaza a Result<T>?
No necesariamente. Un `Result<T>` sigue siendo útil cuando solo necesitas éxito o error, pero union types te dan más flexibilidad cuando hay tres o más variantes legítimas. En muchos casos, `Result<T>` puede ser una de las formas de modelar ese resultado.
¿Conviene usar union types para errores de validación?
Sí, si los errores forman parte del flujo esperado y quieres distinguirlos con claridad. Por ejemplo, no es lo mismo un campo vacío que un formato inválido o un permiso denegado. Cada caso puede ser una variante distinta con datos propios.
¿Esto ayuda en APIs REST?
Sí, porque puedes mapear cada variante a una respuesta HTTP concreta. Eso hace el contrato más claro para frontends, integraciones y otros servicios. También reduce la probabilidad de que un mismo error termine representado de formas distintas.
¿Es buena idea usar union types en todos los DTOs?
No. Si el dato es simple y estable, un DTO normal suele ser mejor. Los union types brillan cuando hay variantes reales y excluyentes, no cuando solo quieres añadir complejidad por anticipado.
¿Qué ventaja tienen frente a una clase base con herencia?
Suelen hacer más explícito el conjunto de variantes y facilitan el pattern matching. La herencia puede funcionar bien, pero a veces deja más espacio para estados inválidos o para jerarquías demasiado pesadas.
¿Sirven para equipos en LatAm que mantienen sistemas legacy?
Sí, sobre todo para introducir mejoras graduales en módulos nuevos o en refactors puntuales. No necesitas migrar todo el sistema; basta con empezar por flujos donde hoy haya demasiados `null`, banderas o excepciones mezcladas.

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