C# 15 - Union types

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.

C# 15 - Collection expression arguments

Collections have been a part of .NET since the very beginning, and there have been both incremental and significant enhancements since then. Likewise C# has evolved to improve how it works with collections.

Consider an array of strings like this:

string[] days = ["Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday", "Sunday"];

Not something we always think about, but it’s a good practice to set the capacity of collections if you know ahead of time how large they need to be. Memory can be allocated up front, rather than incrementally (and depending on the kind of collection it may also avoid needing to reallocate a larger data structure as it grows).

Up until now, you might do that like this for a list that you know will eventually need to hold 7 items, even though initially you’re creating it with 3.

var simpleDayList1 = new List<string>(days.Length);
simpleDayList1.AddRange(days[0..3]);
Console.WriteLine($"Capacity of simpleDayList1: {simpleDayList1.Capacity}, Count: {simpleDayList1.Count}");

The Capacity will be 7.

But what if you’re using collection expressions? Collection expressions are when you use [] to create a collection. The first array above is using them.

Now with C# 15 collection expression arguments, you can include a with() element as part of the collection expression to set constructor arguments, like this:

List<string> dayList = [with(capacity: days.Length), ..days[0..3]];
Console.WriteLine($"Capacity of dayList: {dayList.Capacity}, Count: {dayList.Count}");

The result is the same, but with a more compact syntax.

One thing that confused me at first - I couldn’t figure out how that code was working, as there are 3 constructors defined for List<T> and none of them take a capacity and the items.

I used dotPeek to disassemble the compiled code and this is what it gave me (I had to delete the .pdb otherwise dotPeek was too clever and just showed me the original code!):

List<string> equivalentDayList = new List<string>(days.Length);
equivalentDayList.AddRange((IEnumerable<string>)days[0..3]);

It chose the List<T>(int capacity) constructor. The conversions reference suggests it will call the Add() method to add the items to the collection. I assume there’s an optimisation that will use AddRange() if it is available.

Conclusion

Collection expression arguments are a nice ‘syntactic sugar’ incremental improvement to C#’s support for collections.

More recent posts

Older posts

All posts (currently 1199) can be found in the archive. Jump to recent years in 2026 , 2025 and 2024 .