{"id":59829,"date":"2026-04-02T10:05:00","date_gmt":"2026-04-02T17:05:00","guid":{"rendered":"https:\/\/devblogs.microsoft.com\/dotnet\/?p=59829"},"modified":"2026-04-02T11:09:10","modified_gmt":"2026-04-02T18:09:10","slug":"csharp-15-union-types","status":"publish","type":"post","link":"https:\/\/devblogs.microsoft.com\/dotnet\/csharp-15-union-types\/","title":{"rendered":"Explore union types in C# 15"},"content":{"rendered":"<p>Union types have been frequently requested for C#, and they&#8217;re here. Starting with .NET 11 Preview 2, C# 15 introduces the <code>union<\/code> keyword. The <code>union<\/code> keyword declares that a value is exactly one of a fixed set of types with compiler-enforced exhaustive pattern matching. If you&#8217;ve used discriminated unions in F# or similar features in other languages, you&#8217;ll feel right at home. But C# unions are designed for a C#-native experience: they&#8217;re type unions that compose existing types, integrate with the pattern matching you already know, and work seamlessly with the rest of the language.<\/p>\n<h2>What are union types?<\/h2>\n<p>Before C# 15, when a method needs to return one of several possible types, you had imperfect options. Using <code>object<\/code> placed no constraints on what types are actually stored \u2014 any type could end up there, and the caller had to write defensive logic for unexpected values. Marker interfaces and abstract base classes were better because they restrict the set of types, but they can&#8217;t be &#8220;closed&#8221; \u2014 anyone can implement the interface or derive from the base class, so the compiler can never consider the set complete. And both approaches require the types to share a common ancestor, which doesn&#8217;t work when you wanted a union of unrelated types like <code>string<\/code> and <code>Exception<\/code>, or <code>int<\/code> and <code>IEnumerable&lt;T&gt;<\/code>.<\/p>\n<p>Union types solve these problems. A union declares a closed set of case types \u2014 they don&#8217;t need to be related to each other, and no other types can be added. The compiler guarantees that <code>switch<\/code> expressions handling the union are exhaustive, covering every case type without needing a discard <code>_<\/code> or default branch. But it&#8217;s more than exhaustiveness: unions enable designs that traditional hierarchies can&#8217;t express, composing any combination of existing types into a single, compiler-verified contract.<\/p>\n<p>Here&#8217;s the simplest declaration:<\/p>\n<pre><code class=\"language-csharp\">public record class Cat(string Name);\r\npublic record class Dog(string Name);\r\npublic record class Bird(string Name);\r\n\r\npublic union Pet(Cat, Dog, Bird);<\/code><\/pre>\n<p>This single line declares <code>Pet<\/code> as a new type whose variables can hold a <code>Cat<\/code>, a <code>Dog<\/code>, or a <code>Bird<\/code>. The compiler provides implicit conversions from each case type, so you can assign any of them directly:<\/p>\n<pre><code class=\"language-csharp\">Pet pet = new Dog(\"Rex\");\r\nConsole.WriteLine(pet.Value); \/\/ Dog { Name = Rex }\r\n\r\nPet pet2 = new Cat(\"Whiskers\");\r\nConsole.WriteLine(pet2.Value); \/\/ Cat { Name = Whiskers }<\/code><\/pre>\n<p>The compiler issues an error if you assign an instance of a type that isn&#8217;t one of the case types to a <code>Pet<\/code> object.<\/p>\n<p>The When you use an instance of a <code>union<\/code> type known to be not null, the compiler knows the complete set of case types, so a <code>switch<\/code> expression that covers all of them is exhaustive\u2014 no discard needed:<\/p>\n<pre><code class=\"language-csharp\">string name = pet switch\r\n{\r\n    Dog d =&gt; d.Name,\r\n    Cat c =&gt; c.Name,\r\n    Bird b =&gt; b.Name,\r\n};<\/code><\/pre>\n<p>The types <code>Dog<\/code>, <code>Cat<\/code>, and <code>Bird<\/code> are all non-nullable types. The <code>pet<\/code> variable is known to be non-null, it was set in the earlier snippet. Therefore, this <code>switch<\/code> expression isn&#8217;t required to check for <code>null<\/code>. If any of the types are nullable, for example <code>int?<\/code> or <code>Bird?<\/code>, all <code>switch<\/code> expressions for a <code>Pet<\/code> instance would need a <code>null<\/code> arm for exhaustiveness. If you later add a fourth case type to <code>Pet<\/code>, every <code>switch<\/code> expression that doesn&#8217;t handle it produces a compiler warning. That&#8217;s one core value: the compiler catches missing cases at build time, not at runtime.<\/p>\n<p>Patterns apply to the union&#8217;s <code>Value<\/code> property, not the union struct itself. This &#8220;unwrapping&#8221; is automatic \u2014 you write <code>Dog d<\/code> and the compiler checks <code>Value<\/code> for you. The two exceptions are <code>var<\/code> and <code>_<\/code>, which apply to the union value itself so you can capture or ignore the whole union.<\/p>\n<p>For <code>union<\/code> types, the <code>null<\/code> pattern checks whether <code>Value<\/code> is null. The <code>default<\/code> value of a union struct has a null <code>Value<\/code>:<\/p>\n<pre><code class=\"language-csharp\">Pet pet = default;\r\n\r\nvar description = pet switch\r\n{\r\n    Dog d =&gt; d.Name,\r\n    Cat c =&gt; c.Name,\r\n    Bird b =&gt; b.Name,\r\n    null =&gt; \"no pet\",\r\n};\r\n\/\/ description is \"no pet\"<\/code><\/pre>\n<p>The <code>Pet<\/code> example illustrates the syntax. Now, let&#8217;s explore real world scenarios for union types.<\/p>\n<h3>OneOrMore&lt;T&gt; \u2014 single value or collection<\/h3>\n<p>APIs sometimes accept either a single item or a collection. A union with a body lets you add helper members alongside the case types. The <code>OneOrMore&lt;T&gt;<\/code> declaration includes an <code>AsEnumerable()<\/code> method directly in the union body \u2014 just like you&#8217;d add methods to any type declaration:<\/p>\n<pre><code class=\"language-csharp\">public union OneOrMore&lt;T&gt;(T, IEnumerable&lt;T&gt;)\r\n{\r\n    public IEnumerable&lt;T&gt; AsEnumerable() =&gt; Value switch\r\n    {\r\n        T single =&gt; [single],\r\n        IEnumerable&lt;T&gt; multiple =&gt; multiple,\r\n        null =&gt; []\r\n    };\r\n}<\/code><\/pre>\n<p>Notice that the <code>AsEnumerable<\/code> method must handle the case where <code>Value<\/code> is <code>null<\/code>. That&#8217;s because the default null-state of the <code>Value<\/code> property is <em>maybe-null<\/em>. This rule is necessary to provide proper warnings for arrays of a union type, or instances of the default value for the <code>union<\/code> struct.<\/p>\n<p>Callers pass whichever form is convenient, and <code>AsEnumerable()<\/code> normalizes it:<\/p>\n<pre><code class=\"language-csharp\">OneOrMore&lt;string&gt; tags = \"dotnet\";\r\nOneOrMore&lt;string&gt; moreTags = new[] { \"csharp\", \"unions\", \"preview\" };\r\n\r\nforeach (var tag in tags.AsEnumerable())\r\n    Console.Write($\"[{tag}] \");\r\n\/\/ [dotnet]\r\n\r\nforeach (var tag in moreTags.AsEnumerable())\r\n    Console.Write($\"[{tag}] \");\r\n\/\/ [csharp] [unions] [preview]<\/code><\/pre>\n<h2>Custom unions for existing libraries<\/h2>\n<p>The <code>union<\/code> declaration is an opinionated shorthand. The compiler generates a struct with a constructor for each case type and a <code>Value<\/code> property of type <code>object?<\/code> that holds the underlying value. The constructors enable implicit conversions from any of the case types to the union type. The union instance always stores its contents as a single <code>object?<\/code> reference and boxes value types. That covers the majority of use cases cleanly.<\/p>\n<p>But several community libraries already provide union-like types with their own storage strategies. Those libraries don&#8217;t need to switch to the <code>union<\/code> syntax to benefit from C# 15. Any class or struct with a <code>[System.Runtime.CompilerServices.Union]<\/code> attribute is recognized as a union type, as long as it follows the basic union pattern: one or more public single-parameter constructors (defining the case types) and a public <code>Value<\/code> property.<\/p>\n<p>For performance-sensitive scenarios where case types include value types, libraries can also implement the non-boxing access pattern by adding a <code>HasValue<\/code> property and <code>TryGetValue<\/code> methods. This lets the compiler implement pattern matching without boxing.<\/p>\n<p>For full details on creating custom union types and the non-boxing access pattern, see the <a href=\"https:\/\/learn.microsoft.com\/dotnet\/csharp\/language-reference\/builtin-types\/union#custom-union-types\">union types language reference<\/a>.<\/p>\n<h2>Related proposals<\/h2>\n<p>Union types give you a type that contains one of a closed set of types. Two proposed features provide related functionality for type hierarchies and enumerations. You can learn about both proposals and how they relate to unions by reading the feature specifications:<\/p>\n<ul>\n<li><a href=\"https:\/\/github.com\/dotnet\/csharplang\/blob\/main\/proposals\/closed-hierarchies.md\">Closed hierarchies<\/a>: The <code>closed<\/code> modifier on a class prevents derived classes from being declared outside the defining assembly.<\/li>\n<li><a href=\"https:\/\/github.com\/dotnet\/csharplang\/blob\/main\/proposals\/closed-enums.md\">Closed enums<\/a>: A <code>closed<\/code> enum prevents creation of values other than the declared members.<\/li>\n<\/ul>\n<p>Together, these three features give C# a comprehensive exhaustiveness story:<\/p>\n<ul>\n<li><strong>Union types<\/strong> \u2014 exhaustive matching over a closed set of types<\/li>\n<li><strong>Closed hierarchies<\/strong> \u2014 exhaustive matching over a sealed class hierarchy<\/li>\n<li><strong>Closed enums<\/strong> \u2014 exhaustive matching over a fixed set of enum values<\/li>\n<\/ul>\n<p>Union types are available now in preview. When evaluating them, keep this broader roadmap in mind. These proposals are active, but aren&#8217;t yet committed to a release. Join the discussion as we continue the design and implementation of them.<\/p>\n<h2>Try it yourself<\/h2>\n<p>Union types are available starting with .NET 11 Preview 2. To get started:<\/p>\n<ol>\n<li>Install the <a href=\"https:\/\/dotnet.microsoft.com\/download\/dotnet\">.NET 11 Preview SDK<\/a>.<\/li>\n<li>Create or update a project targeting <code>net11.0<\/code>.<\/li>\n<li>Set <code>&lt;LangVersion&gt;preview&lt;\/LangVersion&gt;<\/code> in your project file.<\/li>\n<\/ol>\n<p>IDE support in Visual Studio will be available in the next Visual Studio Insiders build. It is included in the latest C# DevKit Insiders build.<\/p>\n<p><div class=\"alert alert-info\"><p class=\"alert-divider\"><i class=\"fabric-icon fabric-icon--Info\"><\/i><strong>Early preview: declare runtime types yourself<\/strong><\/p>\nIn .NET 11 Preview 2, the <code>UnionAttribute<\/code> and <code>IUnion<\/code> interface aren&#8217;t included in the runtime yet. You must declare them in your project. Later preview versions will include these types in the runtime.\n<\/div><\/p>\n<p>Add the following to your project (or grab <a href=\"https:\/\/github.com\/dotnet\/docs\/blob\/e68b5dd1e557b53c45ca43e61b013bc919619fb9\/docs\/csharp\/language-reference\/builtin-types\/snippets\/unions\/RuntimePolyfill.cs\">RuntimePolyfill.cs<\/a> from the docs repo):<\/p>\n<pre><code class=\"language-csharp\">namespace System.Runtime.CompilerServices\r\n{\r\n    [AttributeUsage(AttributeTargets.Class | AttributeTargets.Struct,\r\n        AllowMultiple = false)]\r\n    public sealed class UnionAttribute : Attribute;\r\n\r\n    public interface IUnion\r\n    {\r\n        object? Value { get; }\r\n    }\r\n}<\/code><\/pre>\n<p>Once those are in place, you can declare and use union types:<\/p>\n<pre><code class=\"language-csharp\">public record class Cat(string Name);\r\npublic record class Dog(string Name);\r\n\r\npublic union Pet(Cat, Dog);\r\n\r\nPet pet = new Cat(\"Whiskers\");\r\nConsole.WriteLine(pet switch\r\n{\r\n    Cat c =&gt; $\"Cat: {c.Name}\",\r\n    Dog d =&gt; $\"Dog: {d.Name}\",\r\n});<\/code><\/pre>\n<p>Some features from the full <a href=\"https:\/\/learn.microsoft.com\/dotnet\/csharp\/language-reference\/proposals\/unions\">proposal specification<\/a> aren&#8217;t yet implemented, including union member providers. Those are coming in future previews.<\/p>\n<h2>Share your feedback<\/h2>\n<p>Union types are in preview, and your feedback directly shapes the final design. Try them in your projects, explore edge cases, and tell us what works and what doesn&#8217;t.<\/p>\n<p><div  class=\"d-flex justify-content-center\"><a class=\"cta_button_link btn-primary mb-24\" href=\"https:\/\/github.com\/dotnet\/csharplang\/discussions\/9663\" target=\"_blank\">Join the unions discussion on GitHub<\/a><\/div><\/p>\n<p>To learn more:<\/p>\n<ul>\n<li><a href=\"https:\/\/learn.microsoft.com\/dotnet\/csharp\/language-reference\/builtin-types\/union\">Union types \u2014 language reference<\/a><\/li>\n<li><a href=\"https:\/\/learn.microsoft.com\/dotnet\/csharp\/language-reference\/proposals\/unions\">Unions \u2014 feature specification<\/a><\/li>\n<li><a href=\"https:\/\/learn.microsoft.com\/dotnet\/csharp\/whats-new\/csharp-15\">What&#8217;s new in C# 15<\/a><\/li>\n<\/ul>\n","protected":false},"excerpt":{"rendered":"<p>C# 15 introduces union types \u2014 declare a closed set of case types with implicit conversions and exhaustive pattern matching. Try unions in preview today and see the broader exhaustiveness roadmap.<\/p>\n","protected":false},"author":56654,"featured_media":59830,"comment_status":"open","ping_status":"closed","sticky":false,"template":"","format":"standard","meta":{"_acf_changed":false,"footnotes":""},"categories":[685,756],"tags":[7893,46,7631,8140],"class_list":["post-59829","post","type-post","status-publish","format-standard","has-post-thumbnail","hentry","category-dotnet","category-csharp","tag-dotnet-11","tag-c","tag-pattern-matching","tag-union-types"],"acf":[],"blog_post_summary":"<p>C# 15 introduces union types \u2014 declare a closed set of case types with implicit conversions and exhaustive pattern matching. Try unions in preview today and see the broader exhaustiveness roadmap.<\/p>\n","_links":{"self":[{"href":"https:\/\/devblogs.microsoft.com\/dotnet\/wp-json\/wp\/v2\/posts\/59829","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/devblogs.microsoft.com\/dotnet\/wp-json\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/devblogs.microsoft.com\/dotnet\/wp-json\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/devblogs.microsoft.com\/dotnet\/wp-json\/wp\/v2\/users\/56654"}],"replies":[{"embeddable":true,"href":"https:\/\/devblogs.microsoft.com\/dotnet\/wp-json\/wp\/v2\/comments?post=59829"}],"version-history":[{"count":2,"href":"https:\/\/devblogs.microsoft.com\/dotnet\/wp-json\/wp\/v2\/posts\/59829\/revisions"}],"predecessor-version":[{"id":59839,"href":"https:\/\/devblogs.microsoft.com\/dotnet\/wp-json\/wp\/v2\/posts\/59829\/revisions\/59839"}],"wp:featuredmedia":[{"embeddable":true,"href":"https:\/\/devblogs.microsoft.com\/dotnet\/wp-json\/wp\/v2\/media\/59830"}],"wp:attachment":[{"href":"https:\/\/devblogs.microsoft.com\/dotnet\/wp-json\/wp\/v2\/media?parent=59829"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/devblogs.microsoft.com\/dotnet\/wp-json\/wp\/v2\/categories?post=59829"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/devblogs.microsoft.com\/dotnet\/wp-json\/wp\/v2\/tags?post=59829"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}