# Union Types Are Finally Coming to C#

> For years we faked union types with marker interfaces, base classes, and the OneOf library. C# 15 finally bakes them into the language - and here's a quick tour of what they look like and why I think they're a big deal.

Published: 2026-05-30. Author: Milan Jovanović.

Canonical: https://milanjovanovic.tech/blog/union-types-are-finally-coming-to-csharp

C# 15, shipping with .NET 11, adds union types as a preview feature.
A union declares a closed set of types, so a method can return a `User` or a `NotFound` and nothing else.
Pattern matching over it is exhaustive, so the compiler flags every `switch` that misses a case.

Every backend developer eventually hits the same wall: a method that can return _one of several things_.

A parse that either gives you a number or an error.
A lookup that returns a value or "not found".
An operation that succeeds or fails.
In C#, we've never had a clean way to model "this is an `A` **or** a `B`".
So we faked it - with marker interfaces, abstract base classes, tuples, nullable returns, exceptions,
or the excellent [**OneOf**](https://github.com/mcintyre321/OneOf) library.

C# 15 (shipping with .NET 11) finally adds **union types** to the language.
I've wanted this for years, so let me give you a quick tour.

Let's dive in.

## The Problem

Say a method can return a user or fail because they don't exist. Today you'd reach for something like this:

```csharp
// Throw for the "failure" case - control flow via exceptions
public User GetUser(int id) =>
    _users.TryGetValue(id, out var user)
        ? user
        : throw new UserNotFoundException(id);
```

The signature says it returns a `User`, but that's a lie - it might throw instead. The caller has no way to know that without reading the body. The other usual workarounds (a bool `TryGet` with an `out` parameter, a [**custom `Result` class**](https://milanjovanovic.tech/blog/functional-error-handling-in-dotnet-with-the-result-pattern) with nullable fields, or a `OneOf<User, NotFound>`) all add ceremony to express one simple idea.

What you actually want is a **closed set** of types. That's exactly what a union is.

## Declaring a Union

The syntax is delightfully small. You list a name and the case types:

```csharp
public union Result<T>(T, Exception);
```

That's it. A `Result<T>` is now _either_ a `T` _or_ an `Exception` - and nothing else. The types don't even need to be related, which is the whole point.

Here's a more concrete example with unrelated [**record types**](https://milanjovanovic.tech/blog/csharp-records-when-how):

```csharp
public record CreditCard(string Last4, string Brand);
public record PayPal(string Email);
public record BankTransfer(string Iban);

public union PaymentMethod(CreditCard, PayPal, BankTransfer);
```

## Creating Values

There's an implicit conversion from each case type, so you just assign the value directly:

```csharp
PaymentMethod method = new CreditCard("4242", "Visa");
```

Try to assign a type that isn't in the set, and it's a **compile error**. The set is closed.

## Consuming a Union

This is where it shines. Pattern matching just works, and the compiler checks the inner value for you:

```csharp
string Describe(PaymentMethod method) => method switch
{
    CreditCard card  => $"{card.Brand} ending {card.Last4}",
    PayPal paypal    => $"PayPal ({paypal.Email})",
    BankTransfer ach => $"Bank transfer to {ach.Iban}",
}; // No `_` or `default` needed
```

Notice there's **no discard `_` and no `default` arm**. Because the union is closed, the compiler knows all three cases are covered. Forget one, and you get a warning at compile time:

```
warning CS8509: The switch expression does not handle all possible values
of its input type (it is not exhaustive). For example, the pattern 'BankTransfer'
is not covered.
```

That exhaustiveness check is the feature I'm most excited about. Add a new case to the union later, and the compiler points you at every `switch` you forgot to update.

## Back to The Problem

Remember our lying `GetUser` method from earlier? Let's fix it with a union.

First, declare what the method can actually return - a `User` or a `NotFound`:

```csharp
public record NotFound(int Id);

public union UserResult(User, NotFound);
```

Now the signature tells the truth, and there are no exceptions for control flow:

```csharp
public UserResult GetUser(int id) =>
    _users.TryGetValue(id, out var user)
        ? user
        : new NotFound(id);
```

And the caller has to handle both outcomes - the compiler won't let them forget:

```csharp
IResult response = GetUser(42) switch
{
    User user      => Results.Ok(user),
    NotFound found => Results.NotFound($"No user with id {found.Id}"),
};
```

That's the whole pitch. The return type _tells you the truth_: here are exactly the shapes you'll get back, and you can't ignore one by accident. No more reading the method body to discover what it might throw.

## A Few Caveats

This is still a **preview/experimental** feature. A few things to keep in mind:

- It targets **C# 15 / .NET 11**, and the syntax may still change before release. Try it on .NET 11 Preview 4 or later.
- Under the hood, a union is compiled to a `struct` that boxes value-type cases and stores the contents as a single `object?`. There's a non-boxing path for performance-sensitive code, but the default is simple.
- This is a _type_ union (an `A` or a `B`), not a full discriminated union with named cases yet. It covers the vast majority of what I reach for OneOf for today.

## Summary

Union types close a gap that's been open in C# for a very long time.

- Declare a **closed set** of types with `public union Name(A, B, C);`.
- Assign case values **directly** - implicit conversions handle the rest.
- **Pattern match** with full compiler-checked exhaustiveness, no `default` arm required.
- Model results, options, and "one of these" returns **without** marker interfaces, base classes, or extra libraries.

It's a small syntax with a big payoff: your method signatures finally tell the truth about what they return, and the compiler keeps every `switch` honest.

I'll explore this feature more in the future, but I wanted to share this quick tour now that it's available in preview.

Thanks for reading.

And stay awesome!

---

## Frequently asked questions

### Does C# have union types?

C# 15, shipping with .NET 11, introduced union types as a preview/experimental feature, first available in .NET 11 Preview 4. Before that, developers faked unions with marker interfaces, base classes, tuples, exceptions, or the OneOf library.

### How do you declare a union type in C#?

In the C# 15 preview, you list a name and the case types: public union PaymentMethod(CreditCard, PayPal, BankTransfer);. The union is a closed set, assigning any type outside it is a compile error, and the case types do not need to be related.

### Why use a union type instead of throwing an exception?

A method that throws for its failure case has a signature that lies; callers must read the body to learn what it might throw. A union like UserResult(User, NotFound) states exactly which shapes come back, and the compiler forces callers to handle both.

### How does exhaustiveness checking work with C# unions?

Because a union is a closed set, a switch expression over it needs no discard or default arm. If a case is missing, the compiler emits warning CS8509, so adding a new case later points you at every switch you forgot to update.

### Are C# 15 union types full discriminated unions?

No. The preview feature was a type union (an A or a B), not a discriminated union with named cases. It still covered most scenarios developers previously used the OneOf library for, and the syntax could change before release.

### How are C# union types implemented under the hood?

In the C# 15 preview, a union compiles to a struct that stores its contents as a single nullable object, boxing value-type cases by default. A non-boxing path exists for performance-sensitive code, but the default implementation favors simplicity.
