In an August 2025 video, Mads Torgersen -lead designer for the C# Programming Language- outlined the C# team’s plans for C# unions. The team aims at potentially introduce this feature with the C# 15 release in November 2026. This is not a commitment but rather a possibility—one that seems quite likely, given how straightforward the plan appears to be. The video is here and he talks about union from 1:02:25 till 1:10:30.
C# Unions Plan
Mads introduces this code to illustrate their plans for unions:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
Pet pet = new Dog("Rover"); var description = pet switch { Dog(var name) => name, Cat(var adjective) => $"A {adjective} cat", Bird bird => $"A {bird.Species}", }; public union Pet (Dog, Cat, Bird); public record class Dog(string Name); public record struct Cat(string Adjective); public record class Bird(string Species); public record class Shark(int Teeth); |
This code feels very idiomatic to C#. Here are some remarks:
- The type
Pet
is defined as a union of the existing typesDog
,Cat
, andBird
. Once declared, there’s no need to repeatedly specify the constituent types—Pet
fully encapsulates that definition. - The first line,
Pet pet = new Dog("Rover");
means that the compiler will know somehow there is an implicit conversion fromDog
toPet
, sincePet
is not a base class forDog
. - Unlike some languages such as F#, the types that make up a union in C# must be defined separately from the union itself. For comparison, in F# a union can be declared as follows, where
Car
,Truck
, andBicycle
are part of theVehicle
union. They can be seen as tags to represent the various cases, instead of being seen as type. This is why, in functional languages, they are called discriminated union or tag union, whereas in the approach chosen by the C# team, it is closer to a structural union or type union.
1 2 3 4 |
type Vehicle = | Car of model : string * year : int | Truck of model : string * year : int * payload : float | Bicycle of model : string |
- A key benefit is how exhaustiveness checking works with pattern matching in the
switch
clause. Since the compiler knows thatPet
can only ever be aDog
,Cat
, orBird
, aswitch
expression overPet
will not produce warnings once all cases are handled. - If we later extend the union by adding, for example,
Shark
, the compiler will warn (or error?) everyswitch
onPet
that doesn’t account for the new case. This makesPet
a closed union type: its set of possibilities is fixed and enforced by the compiler. This is in contrast to class inheritance, where new subclasses can be introduced without breaking existing code thanks to polymorphism applied on virtual members. This is the Open-Close Principle (OCP): open for extension, closed for modification. In a word OCP applies to class inheritance but not to unions.
C# Union Plan Behind the Scene
The C# team proposes implementing union types as a record struct
with a single read-only object? Value
property. This design is both simple and powerful, since in C# virtually any type can be represented as an object
reference: class, struct, interface, delegate, enumeration, primitive types… but not unmanaged pointers for example.
1 2 3 4 5 6 7 8 9 10 |
#region public union Pet (Dog, Cat, Bird); public partial record struct Pet : IUnion { public Pet(Dog value) => Value = value; public Pet(Cat value) => Value = value; public Pet(Bird value) => Value = value; public object? Value { get; } } #endregion |
Could C# Unions be Smart for Value Type Only Unions?
The trade-off is that value types included in the union, such as Cat
, will be boxed when stored in the Value
property. The object used to box the value must then be managed by the garbage collector, introducing a small performance overhead.
A more sophisticated implementation of unions could optimize storage for value types that share the same memory footprint (i.e., represented by the same number of bytes). Alternatively, the runtime could allocate space based on the largest value type in the union, with smaller types incurring padding overhead. However, either approach would significantly complicate the implementation. At 1:08:20 Mads explains that the C# Team will provide a way to manually define the layout of your value type only union.
I wish this could be transparently improved. A method T ValueAs<T>() where T : struct
could be added to Pet
. In the same vein C# 13 eliminated object allocations for params
collections. I believe that unions composed solely of value types are likely to be a common scenario, like for example parser implementation or messaging protocol.
Let’s mention that modern C# and the class MemoryMarshal
makes it easy to cast a range of byte into any value type instance:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
using System.Diagnostics; using System.Runtime.InteropServices; Span<byte> bytes8 = stackalloc byte[8] { 2, 0, 0, 0, 1, 0, 0, 0 }; // Convert first 4 bytes to uint uint value32 = MemoryMarshal.Read<uint>(bytes8); Debug.Assert(value32 == 2); // Convert all 8 bytes to ulong ulong value64 = MemoryMarshal.Read<ulong>(bytes8); Debug.Assert(value64 == 4294967298); // Convert all 8 bytes to MyStruct ref MyStruct myStruct = ref MemoryMarshal.AsRef<MyStruct>(bytes8); Debug.Assert(myStruct.X == 2); Debug.Assert(myStruct.Y == 1); [StructLayout(LayoutKind.Sequential)] struct MyStruct { public uint X; public uint Y; } |
However, this struct below cannot be stored in bytes8
because the string
reference it contains is managed by the garbage collector and cannot be safely written into stackalloc
memory.
1 2 |
[StructLayout(LayoutKind.Sequential)] struct MyStruct { public string X; } |
Pattern Matching switch over C# Union
Based on the C# team’s plan, enabling pattern-matching switch
on a C# union is straightforward. A call to the Value
property must be inferred by the compiler.
1 2 3 4 5 |
var description = pet.Value switch { Dog(var name) => name, Cat(var adjective) => $"A {adjective} cat", Bird bird => $"A {bird.Species}", }; |
C# Unions: More Precision
On the Reddit thread discussing this post, Matt Warren from the C# team provided some clarifications, which are transcribed below. Italics indicate points that don’t add new information beyond what we already knew from Mads.
The feature, as proposed, would allow you to declare named typed unions that list a set of types that the union can represent. The union is actually a struct that wraps an object field. Its constructors limit the kinds of values that can be held by the union. There is no erasure going on, but cases that are value types will be boxed.
1 |
public union Pet(Cat, Dog, Bird); |
Emits as:
1 2 3 4 5 6 |
public struct Pet : IUnion { public Pet(Cat value) { this.Value = value; } public Pet(Dog value) { this.Value = value; } public Pet(Bird value) { this.Value = value; } public object? Value { get; } } |
You can declare a discriminated union over the type union using case declarations within braces. Each case becomes a nested record type.
1 2 3 4 5 |
public union Pet { case Cat(string Name, string Personality); case Dog(string Name, string Breed); case Bird(string Name, string Species); } |
You can assign an instance of a case directly to a union variable and when you pattern match over a union instance, the value of the union is accessed via the Value property. Because the compiler knows the closed set of types, a switch can be exhaustive, so no need for default cases.
1 2 3 4 5 6 |
Pet pet = new Dog("Spot", "Dalmation"); var _ = pet switch { Cat c => ..., Dog d => ..., Bird b => ... } |
You will be able to define your own types that will be recognized by the compiler as unions. You may declare the layout of the type in ways that avoid boxing if you choose. The compiler will recognize other methods that access the value that will also avoid boxing.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
public struct IntOrString : IUnion { private readonly int _kind; private readonly int _value1; private readonly string _value2; public IntOrString(int value) { _kind = 1; _value1 = value; } public IntOrString(string value) { _kind = 2; _value2 = value; } // still needs to exist for IUnion public object? Value => _kind switch { 1 => value1, 2 => value2, _ => null }; // access pattern that avoids boxing. public bool HasValue => _kind != 0; public bool TryGetValue(out int value) { ... } public bool TryGetValue(out string value) { ... } } |
Future version of the language may include more kinds of unions that auto generate non-boxing layouts for you, like when records first released and later record structs were added.
A set of predeclared standard generic unions will exist in the runtime for scenarios that don’t require dedicated named unions. These will have the boxing behaviors.
1 2 3 4 5 |
public union Union<T1, T2>(T1, T2); public union Union<T1, T2, T3>(T1, T2, T3); public union Union<T1, T2, T3, T4>(T1, T2, T3, T4); ... internal void Ride(Union<Animal, Automobile> conveyance) {...} |
Conclusion
Again, Mads did not promise that unions will be part of C# 15 in November 2026. He explained that work will begin once C# 14 reaches general availability in November 2025 and hopefully they will make it.
Also he mentioned that they will collaborate with the .NET Core Base Class Library team so that many union types will already be available when this language feature is released
At the end of the talk, someone asked about interoperability between F# unions and C# unions. Since they are fundamentally different (see the explanation above on F# discriminated unions vs. C# type unions), the interoperability model is not straightforward. Nevertheless the team plans to work on it. Mads also added that they might allow defining the types that make up a C# union directly within the union’s scope, similar to F#.
In 2024 we already wrote a blog post named C# Discriminated Union to explain in details the concept to C# developers: C# Discriminated Union: What’s Driving the C# Community’s Inquiries?. Hopefully, things are becoming clearer, and we will update this post with the latest progress from the C# team on this topic.