DEV Community

Vikrant Bagal
Vikrant Bagal

Posted on

C15 Union Types: The Pattern Matching Feature Finally Here

C# 15 Union Types: The Pattern Matching Feature Finally Here

TL;DR: C# 15 introduces union types - a compiler-enforced way to represent values that can be one of several types. With exhaustive pattern matching and no inheritance requirements, this feature replaces result patterns, marker interfaces, and complex error handling.


If you've been waiting for C# to catch up with functional programming features, your wait is finally over. C# 15 introduces union types - a language feature that's been requested for years and is now part of .NET 11 preview.

Microsoft recently announced this feature as part of C# 15, and it's already making waves in the developer community. Here's everything you need to know about this game-changing addition.

What Problem Does This Solve?

Imagine writing code where you know a method can return exactly one of several specific types. Currently, C# forces you to choose between:

  • Generic Result<T, E> classes
  • Exception handling (even for expected errors)
  • Complex inheritance hierarchies with marker interfaces

Union types offer a cleaner, more explicit alternative with compile-time exhaustiveness checking.

The Basic Syntax

Declare a union type with the union keyword:

public union Pet(Cat, Dog, Bird);
Enter fullscreen mode Exit fullscreen mode

This creates a Pet union that can hold a Cat, Dog, or Bird. The compiler then ensures that every switch expression handles all three cases.

Real-World Example: API Responses

Let's say you're building an authentication service:

public union AuthResponse(User authenticatedUser, string errorMessage);

AuthResponse Authenticate(string token)
{
    if (IsValidToken(token))
    {
        var user = GetUserFromToken(token);
        return new AuthResponse(user, null!);
    }

    return new AuthResponse(null!, "Invalid or expired token");
}
Enter fullscreen mode Exit fullscreen mode

No more exceptions for expected failures. No more generic result wrappers. Just explicit, typed outcomes.

Exhaustive Pattern Matching

The real power comes from switch expressions that must handle all cases:

AuthResponse response = Authenticate(userToken);

switch (response)
{
    case AuthResponse.User user:
        Console.WriteLine($"Welcome back, {user.Name}!");
        break;
    case AuthResponse.ErrorMessage error:
        Console.WriteLine($"Error: {error.Message}");
        break;
}
Enter fullscreen mode Exit fullscreen mode

Notice what's different: The compiler requires both cases. Miss one? You get a compile-time error. No more runtime surprises from unhandled authentication states.

Replacing Try-Pattern Methods

C# developers are used to the TryGetValue and TryParse pattern:

// The old way
if (dict.TryGetValue(key, out var value))
{
    Console.WriteLine(value);
}
else
{
    // Handle missing key
}

// Or the new union way
public union GetValueResult<T>(T value, KeyNotFoundError error);
Enter fullscreen mode Exit fullscreen mode

Error Handling Without Exceptions

One of the most compelling use cases is replacing "exceptions as control flow":

// Traditional way - exceptions for expected errors
public User GetUser(string id)
{
    var user = _repository.Find(id);
    if (user == null)
        throw new UserNotFoundException(id);
    return user;
}

// Union type way - explicit error types
public union GetUserResult(User user, UserNotFoundException error);
Enter fullscreen mode Exit fullscreen mode

This makes your code more predictable and easier to reason about. Errors become first-class citizens rather than hidden control flow.

Performance Considerations

According to Microsoft's documentation:

  • Zero runtime overhead - unions compile to the same IL as equivalent code
  • Better type safety at compile time
  • Smaller code footprint - no need for inheritance hierarchies

The feature is implemented efficiently, making it production-ready from day one.

Three Primary Use Cases (from Microsoft)

1. Result-or-Error Returns

public union ProcessResult(T success, Error error);
Enter fullscreen mode Exit fullscreen mode

Perfect for operations that have two distinct outcomes.

2. Message or Command Dispatching

public union Command(Login, Logout, UpdateProfile);
Enter fullscreen mode Exit fullscreen mode

With exhaustive matching, you'll get warnings if you add a new command but forget to handle it everywhere.

3. Replacing Marker Interfaces

// Old way - create interface just for pattern matching
public interface IErrorResponse { }

// New way - just use a union
public union ApiResponse(Data, ErrorResponse);
Enter fullscreen mode Exit fullscreen mode

Code Comparison

Before: Generic Result Type

public readonly struct Result<T, E>
{
    private readonly T _success;
    private readonly E _error;

    // Constructor, properties, etc.
}

// Usage everywhere
public Result<User, UserNotFound> GetUser(string id)
Enter fullscreen mode Exit fullscreen mode

After: Union Type

public union GetUserResult(User user, UserNotFound error);

// Simple, explicit, compile-checked
Enter fullscreen mode Exit fullscreen mode

Common Pitfalls to Avoid

1. Don't Overuse

Union types work best with closed sets of 2-10 cases. For larger or dynamic sets, stick with existing patterns.

2. Watch Nullability

Be explicit about which cases can be null:

public union Result(User user, string? error);
Enter fullscreen mode Exit fullscreen mode

3. Keep Cases Small

Each case should represent a single, focused concern. Don't create union LargeComplexType(SmallError, BigProblem).

Best Practices

  1. Use descriptive names: UserResponse(User, ValidationError) beats Response(T, string)
  2. Document error cases: XML docs explain when each error occurs
  3. Group related errors: Keep similar error types together
  4. Rely on compiler warnings: Let exhaustiveness checking catch missing cases

Migration Path

If you currently use:

  • TryXxx patterns → Union types with explicit error states
  • Generic Result<T, E> → Native union syntax
  • Marker interfaces for pattern matching → Direct unions

The Bottom Line

Union types bring C# one step closer to the type safety of functional languages like F# and Haskell, while maintaining the pragmatic approach C# developers expect.

For developers tired of working around C# limitations in error handling and pattern matching, this feature is a breath of fresh air. It's explicit, safe, and zero-overhead.

Ready to try it? You can experiment with C# 15 union types using the latest Visual Studio 2026 Insiders build or the .NET 11 preview SDK.


What do you think? Are you going to start using union types in your projects? What's the first feature you'd replace with this? Let me know in the comments!

💡 About the author: https://www.linkedin.com/in/vikrant-bagal

dotnet #csharp #programming #softwareengineering #webdev

Top comments (0)