Sometimes a value is one of a small, fixed set of unrelated types: a pet is a cat, a dog or a bird, and nothing else. Before C# 15 you had to model this with a shared base class or interface (which the case types might not be able to adopt) or fall back to object and lose all compiler help.
C# 15 introduces union types using the union keyword. A union lists its case types, gets implicit conversions from each of them, and lets the compiler check that a switch expression handles every case. It is a union of existing types rather than a tagged union: patterns match against the value held inside the union, so you switch on Dog or Cat directly.
Code
csharp
Pet pet = new Dog("Rex");
string name = pet switch
{
Dog d => d.Name,
Cat c => c.Name,
Bird b => b.Name,
// No default arm needed, the compiler knows these are all the cases
};
public record class Cat(string Name);
public record class Dog(string Name);
public record class Bird(string Name);
public union Pet(Cat, Dog, Bird);csharp
// Nothing stops other types being passed and the compiler can't check coverage
object pet = new Dog("Rex");
string name = pet switch
{
Dog d => d.Name,
Cat c => c.Name,
Bird b => b.Name,
_ => throw new InvalidOperationException("Unexpected pet type"),
};
public record class Cat(string Name);
public record class Dog(string Name);
public record class Bird(string Name);Notes
- Case types do not need to share a base type or interface
- A
uniondeclaration compiles to a struct that stores its contents in a singleobject?Valueproperty, so value-type cases such asintare boxed - A
defaultunion has anullValue, so when the compiler can't rule that out it warns about an unhandlednulleven if every case type is covered - See also closed hierarchies for exhaustiveness across a class hierarchy