C# 15 Adds Union Types in .NET 11 Preview 2

C# 15 finally ships union types, a feature long requested by the community. The initial support landed in .NET 11 preview 2 and has been refined through preview 4. This post covers syntax, usage, generated code, and limitations.

What Are Union Types?

Union types represent a value that can be one of several distinct types. They are common in functional languages like F#, TypeScript, and Rust. Classic examples include Option (Some or None) and Result (Success or Error). In C#, developers previously used base classes, object, or manual tagging to achieve similar behavior.

Union Syntax in C# 15

Define a union using the union keyword followed by the list of member types:

public record Windows(string Version);
public record Linux(string Distro, string Version);
public record MacOS(string Name, int Version);

public union SupportedOS(Windows, Linux, MacOS);

Create instances via constructor or implicit conversion:

// Explicit constructor
SupportedOS os1 = new SupportedOS(new MacOS("Tahoe", 25));

// Implicit conversion (compiler rewrites to constructor call)
SupportedOS os2 = new MacOS("Tahoe", 25);

Exhaustive switch expressions work without a discard case:

string GetDescription(SupportedOS os) => os switch
{
    Windows w => $"Windows {w.Version}",
    Linux l => $"{l.Distro} {l.Version}",
    MacOS m => $"MacOS {m.Name} ({m.Version})",
};

The compiler warns if a case is missing: warning CS8509: The switch expression does not handle all possible values of its input type (it is not exhaustive).

Enabling Union Types in Your Project

  1. Install .NET 11 preview 2 SDK or later (preview 4+ recommended).
  2. Set preview in your .csproj:

  
    Exe
    preview
    net11.0;net8.0;net48
    enable
    enable
  

You can target older runtimes (e.g., .NET 8, .NET Framework 4.8) because union support is a compiler feature. For older runtimes, add the following helper types manually:

#if !NET11_0_OR_GREATER
namespace System.Runtime.CompilerServices;

[AttributeUsage(Class | Struct, AllowMultiple = false, Inherited = false)]
public sealed class UnionAttribute : Attribute;

public interface IUnion { object? Value { get; } } #endif


These types are built into .NET 11 preview 4+.

### How Unions Are Implemented

The compiler generates a struct decorated with `[Union]` implementing `IUnion`:

```csharp
[Union]
public struct SupportedOS : IUnion
{
    public object? Value { get; }

    public SupportedOS(Windows value) => this.Value = (object)value;
    public SupportedOS(Linux value) => this.Value = (object)value;
    public SupportedOS(MacOS value) => this.Value = (object)value;
}

The [Union] attribute drives implicit conversions and switch exhaustiveness. Without it, the same struct fails to compile with implicit conversion or pattern matching.

Boxing and Performance

The generated struct stores the case value as object?, causing boxing for value types. For example:

public union IntOrBool(int, bool);

Results in:

public IntOrBool(int value) => this.Value = (object)value; // boxing!
public IntOrBool(bool value) => this.Value = (object)value; // boxing!

This allocation may be unacceptable in hot paths. Developers can create custom union implementations that avoid boxing by using a tagged union with explicit storage (e.g., a struct with a discriminant and fields for each case). The [Union] attribute and IUnion interface allow custom types to participate in the same language support.

Custom Union Types

You can implement your own union type by decorating a struct with [Union] and implementing IUnion. This is useful if you need to avoid boxing, or if you want to integrate existing libraries like OneOf or Sasa.

[Union]
public struct MyUnion : IUnion
{
    public object? Value { get; }
    // Custom constructors, possibly with unboxed storage
}

Limitations and Considerations

  • Boxing of value types is the main performance concern. The compiler-generated union always boxes.
  • The Value property is typed as object?, so you lose type safety when accessing it directly.
  • IDE support is available in Visual Studio Preview and VS Code C# DevKit Insiders; JetBrains Rider support is pending.
  • The feature is still in preview; details may change before the final .NET 11 release.

Summary

C# 15 union types provide a long-awaited, ergonomic way to model sum types. The union keyword, implicit conversions, and exhaustive switch expressions make the feature feel native. However, the boxing overhead for value types means power users may still opt for custom implementations. Try it out with .NET 11 preview 4 and give feedback to the Roslyn team.


This article is based on the .NET 11 preview 4 release. Check the official spec for updates.