This is something some people have been wanting in C# for a really long time.
Up until now, if you wanted an object that could be one of a limited number of types, you might have resorted to inheritance like this:
public abstract record Pet;
public record Dog : Pet;
public record Cat : Pet;
The downside of this is that you’re implying there’s a hierarchy.
Union types solve this problem. Here are a couple of union types for Drill and Tool.
public enum HammerType
{
Claw,
Sledge
}
public record Hammer(HammerType Type);
public enum ScrewdriverType
{
Flathead,
Phillips
}
public record Screwdriver(ScrewdriverType Type);
public record PowerDrill(bool Hammer, bool Reversible, bool Cordless);
public record ManualDrill();
public union Drill(PowerDrill, ManualDrill);
public union Tool(Hammer, Screwdriver, Drill);
A Drill can be either PowerDrill or ManualDrill. They don’t have any common properties so there’s no need to make one inherit from the other, or have a common base class.
Using union types is straightforward:
Tool myTool = new Drill(new PowerDrill(true, true, false));
switch (myTool)
{
case Hammer h:
Console.WriteLine($"Hammer: {h.Type}");
break;
case Screwdriver s:
Console.WriteLine($"Screwdriver: {s.Type}");
break;
case Drill d:
switch (d)
{
case PowerDrill pd:
Console.WriteLine($"PowerDrill: Hammer={pd.Hammer}, Reversible={pd.Reversible}, Cordless={pd.Cordless}");
break;
case ManualDrill md:
Console.WriteLine("ManualDrill");
break;
}
break;
}
Note that the Tool type doesn’t directly include all the types that make up the Drill type.
If you use a switch expression with a union type and don’t cover all possible types, the compiler can detect this and generate a warning:
var t = myTool switch
{
Hammer h => $"Hammer: {h.Type}",
Screwdriver s => $"Screwdriver: {s.Type}",
Drill d => d switch
{
PowerDrill pd => $"PowerDrill: Hammer={pd.Hammer}, Reversible={pd.Reversible}, Cordless={pd.Cordless}",
//ManualDrill md => "ManualDrill"
}
};
Causes this warning:
warning CS8509: The switch expression does not handle all possible values of its input type (it is not exhaustive). For example, the pattern 'ManualDrill' is not covered
Kinds of Union types
Union types exist in a number of programming languages, and there are apparently two kinds - “tagged” (also known as discriminated) and “untagged”.
I did a bit of reading up to understand the difference:
Untagged unions
This is what C# 15 provides. C# union types are “unions of types”.
You check the type to identify what it is.
Tagged or discriminated unions
F# has these.
type ApiResult =
| Success of body: string
| Failure of statusCode: int * message: string
type PersonName =
| FirstName of string
| LastName of string
Note how a discriminated union allows you to define a type which is either of two strings. The ‘discriminator’ (or ‘tag’) is separate to the value’s type.
Conclusion
I think union types provide the opportunity to be much more expressive and precise when defining the shape of the data structures you’re working with in C#. It also means you can ‘lean on the compiler’ to ensure a union type’s possible values are all handled correctly.