Minimal, allocation-free discriminated unions for C#, aligned with the C# unions language proposal. It's designed to be drop-in compatible with the upcoming native feature. Giving you modern, idiomatic C# union types today.
$ dotnet add package Qtopia.UnionsQtopia.Unions provides minimal, allocation-free discriminated unions for C#, aligned with the official C# unions language proposal.
It's designed to be drop-in compatible with the upcoming native feature - giving you modern, idiomatic C# union types today.
C# doesn't yet support native union types, but the design more or less defined in the official proposal. This library implements that proposal's shape and semantics with minimal overhead:
Union<T1, T2> through Union<T1, ..., T9>
Each union is an immutable readonly struct that supports:
T1 ... Tn).TryGet(out Tn) to safely extract a specific case.Value exposing the contained object for pattern matching.ToString() forwarding to the stored value (or "null").Add Unions from NuGet:
dotnet add package Qtopia.Unions
Or via the Visual Studio Package Manager:
Install-Package Qtopia.Unions
using Pet = Union<Cat, Dog, Bird>;Pet pet = new Dog("Rex");
pet = new Cat("Millie");
// or using implicit conversions
Pet other = new Bird("Tweety");if (pet.TryGet(out Dog dog))
Console.WriteLine($"Dog: {dog.Name}");
else if (pet.TryGet(out Cat cat))
Console.WriteLine($"Cat: {cat.Name}");
else if (pet.TryGet(out Bird bird))
Console.WriteLine($"Bird: {bird.Name}");.Valueswitch (pet.Value)
{
case Dog d:
Console.WriteLine($"Dog: {d.Name}");
break;
case Cat c:
Console.WriteLine($"Cat: {c.Name}");
break;
case Bird b:
Console.WriteLine($"Bird: {b.Name}");
break;
case null:
Console.WriteLine("Pet is null");
break;
}Union<string, int> ParseOrEcho(string input)
=> int.TryParse(input, out var number) ? number : input;
var result = ParseOrEcho("42");
if (result.TryGet(out int n))
Console.WriteLine($"Number: {n}");
else if (result.TryGet(out string s))
Console.WriteLine($"Text: {s}");| Member | Description |
|---|---|
| Constructors | new Union<T1,...>(Tn value) for each supported type. |
| Implicit conversion | Automatically converts any Tn into the union type. |
| TryGet | bool TryGet(out Tn value) safely extracts a case value. |
| Value | object? Value { get; } exposes the stored value for inspection or pattern matching. |
| ToString() | Delegates to the underlying value's ToString() or returns "null". |
You can store null for any reference type:
Union<string, int> u = (string)null;
TryGet(out string s) returns true and s == null.
Pattern matching on .Value will correctly enter the case null: arm.
Internally, a numeric tag tracks which type is active, so nulls are never ambiguous even when multiple Tn are reference types.
object field).TryGet prevents invalid casts.Union types are declared as readonly structs.Use using aliases to name common unions in your domain:
using Amount = Union<int, decimal>;
using Search = Union<User, Group, Organization>;
Combine with switch expressions for concise logic:
int Size(Union<string, byte[]> data) => data.Value switch
{
string s => s.Length,
byte[] b => b.Length,
null => 0,
_ => 0
};
Union<T1,T2> ... Union<T1,...,T9>).
The pattern can be extended if needed.Unit tests use xUnit with FluentAssertions and demonstrate usage patterns for all supported union types.
dotnet testDistributed under the MIT license. See the included LICENSE file for details.