{"id":3777,"date":"2026-04-02T17:50:18","date_gmt":"2026-04-02T17:50:18","guid":{"rendered":"https:\/\/rssfeedtelegrambot.bnaya.co.il\/index.php\/2026\/04\/02\/explore-union-types-in-c-15\/"},"modified":"2026-04-02T17:50:18","modified_gmt":"2026-04-02T17:50:18","slug":"explore-union-types-in-c-15","status":"publish","type":"post","link":"https:\/\/rssfeedtelegrambot.bnaya.co.il\/index.php\/2026\/04\/02\/explore-union-types-in-c-15\/","title":{"rendered":"Explore union types in C# 15"},"content":{"rendered":"<p>Union types have been frequently requested for C#, and they\u2019re 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\u2019ve used discriminated unions in F# or similar features in other languages, you\u2019ll feel right at home. But C# unions are designed for a C#-native experience: they\u2019re 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\u2019t be \u201cclosed\u201d \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\u2019t 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\u2019t 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\u2019s more than exhaustiveness: unions enable designs that traditional hierarchies can\u2019t express, composing any combination of existing types into a single, compiler-verified contract.<\/p>\n<p>Here\u2019s the simplest declaration:<\/p>\n<pre><code class=\"language-csharp\">public record class Cat(string Name);\npublic record class Dog(string Name);\npublic record class Bird(string Name);\n\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\");\nConsole.WriteLine(pet.Value); \/\/ Dog { Name = Rex }\n\nPet pet2 = new Cat(\"Whiskers\");\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\u2019t one of the case types to a <code>Pet<\/code> object.<\/p>\n<p>When you use an instance of a <code>union<\/code> type, 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\n{\n    Dog d =&gt; d.Name,\n    Cat c =&gt; c.Name,\n    Bird b =&gt; b.Name,\n};<\/code><\/pre>\n<p>The types <code>Dog<\/code>, <code>Cat<\/code>, and <code>Bird<\/code> are all non-nullable types, the <code>switch<\/code> expression isn\u2019t required to check for <code>null<\/code>. If any of the types are nullable, for example <code>int?<\/code> or <code>Bird?<\/code>, the <code>switch<\/code> expression 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\u2019t handle it produces a compiler warning. That\u2019s one core value: the compiler catches missing cases at build time, not at runtime.<\/p>\n<p>Patterns apply to the union\u2019s <code>Value<\/code> property, not the union struct itself. This \u201cunwrapping\u201d 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;\n\nvar description = pet switch\n{\n    Dog d =&gt; d.Name,\n    Cat c =&gt; c.Name,\n    Bird b =&gt; b.Name,\n    null =&gt; \"no pet\",\n};\n\/\/ description is \"no pet\"<\/code><\/pre>\n<p>The <code>Pet<\/code> example illustrates the syntax. Now, let\u2019s 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\u2019d add methods to any type declaration:<\/p>\n<pre><code class=\"language-csharp\">public union OneOrMore&lt;T&gt;(T, IEnumerable&lt;T&gt;)\n{\n    public IEnumerable&lt;T&gt; AsEnumerable() =&gt; Value switch\n    {\n        T single =&gt; [single],\n        IEnumerable&lt;T&gt; multiple =&gt; multiple,\n        null =&gt; []\n    };\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\u2019s 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\";\nOneOrMore&lt;string&gt; moreTags = new[] { \"csharp\", \"unions\", \"preview\" };\n\nforeach (var tag in tags.AsEnumerable())\n    Console.Write($\"[{tag}] \");\n\/\/ [dotnet]\n\nforeach (var tag in moreTags.AsEnumerable())\n    Console.Write($\"[{tag}] \");\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\u2019t 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\u2019t 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\n<div class=\"alert alert-info\">\n<p class=\"alert-divider\"><i class=\"fabric-icon fabric-icon--Info\"><\/i><strong>Early preview: declare runtime types yourself<\/strong><\/p>\n<p>In .NET 11 Preview 2, the <code>UnionAttribute<\/code> and <code>IUnion<\/code> interface aren\u2019t included in the runtime yet. You must declare them in your project. Later preview versions will include these types in the runtime.\n<\/p><\/div>\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\n{\n    [AttributeUsage(AttributeTargets.Class | AttributeTargets.Struct,\n        AllowMultiple = false)]\n    public sealed class UnionAttribute : Attribute;\n\n    public interface IUnion\n    {\n        object? Value { get; }\n    }\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);\npublic record class Dog(string Name);\n\npublic union Pet(Cat, Dog);\n\nPet pet = new Cat(\"Whiskers\");\nConsole.WriteLine(pet switch\n{\n    Cat c =&gt; $\"Cat: {c.Name}\",\n    Dog d =&gt; $\"Dog: {d.Name}\",\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\u2019t 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\u2019t.<\/p>\n\n<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>\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\u2019s new in C# 15<\/a><\/li>\n<\/ul>\n<p>The post <a href=\"https:\/\/devblogs.microsoft.com\/dotnet\/csharp-15-union-types\/\">Explore union types in C# 15<\/a> appeared first on <a href=\"https:\/\/devblogs.microsoft.com\/dotnet\">.NET Blog<\/a>.<\/p>","protected":false},"excerpt":{"rendered":"<p>Union types have been frequently requested for C#, and they\u2019re here. Starting with .NET 11 Preview 2, C# 15 introduces [&hellip;]<\/p>\n","protected":false},"author":1,"featured_media":94,"comment_status":"","ping_status":"","sticky":false,"template":"","format":"standard","meta":{"site-sidebar-layout":"default","site-content-layout":"","ast-site-content-layout":"default","site-content-style":"default","site-sidebar-style":"default","ast-global-header-display":"","ast-banner-title-visibility":"","ast-main-header-display":"","ast-hfb-above-header-display":"","ast-hfb-below-header-display":"","ast-hfb-mobile-header-display":"","site-post-title":"","ast-breadcrumbs-content":"","ast-featured-img":"","footer-sml-layout":"","ast-disable-related-posts":"","theme-transparent-header-meta":"","adv-header-id-meta":"","stick-header-meta":"","header-above-stick-meta":"","header-main-stick-meta":"","header-below-stick-meta":"","astra-migrate-meta-layouts":"default","ast-page-background-enabled":"default","ast-page-background-meta":{"desktop":{"background-color":"var(--ast-global-color-4)","background-image":"","background-repeat":"repeat","background-position":"center center","background-size":"auto","background-attachment":"scroll","background-type":"","background-media":"","overlay-type":"","overlay-color":"","overlay-opacity":"","overlay-gradient":""},"tablet":{"background-color":"","background-image":"","background-repeat":"repeat","background-position":"center center","background-size":"auto","background-attachment":"scroll","background-type":"","background-media":"","overlay-type":"","overlay-color":"","overlay-opacity":"","overlay-gradient":""},"mobile":{"background-color":"","background-image":"","background-repeat":"repeat","background-position":"center center","background-size":"auto","background-attachment":"scroll","background-type":"","background-media":"","overlay-type":"","overlay-color":"","overlay-opacity":"","overlay-gradient":""}},"ast-content-background-meta":{"desktop":{"background-color":"var(--ast-global-color-5)","background-image":"","background-repeat":"repeat","background-position":"center center","background-size":"auto","background-attachment":"scroll","background-type":"","background-media":"","overlay-type":"","overlay-color":"","overlay-opacity":"","overlay-gradient":""},"tablet":{"background-color":"var(--ast-global-color-5)","background-image":"","background-repeat":"repeat","background-position":"center center","background-size":"auto","background-attachment":"scroll","background-type":"","background-media":"","overlay-type":"","overlay-color":"","overlay-opacity":"","overlay-gradient":""},"mobile":{"background-color":"var(--ast-global-color-5)","background-image":"","background-repeat":"repeat","background-position":"center center","background-size":"auto","background-attachment":"scroll","background-type":"","background-media":"","overlay-type":"","overlay-color":"","overlay-opacity":"","overlay-gradient":""}},"footnotes":""},"categories":[7],"tags":[],"class_list":["post-3777","post","type-post","status-publish","format-standard","has-post-thumbnail","hentry","category-dotnet"],"_links":{"self":[{"href":"https:\/\/rssfeedtelegrambot.bnaya.co.il\/index.php\/wp-json\/wp\/v2\/posts\/3777","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/rssfeedtelegrambot.bnaya.co.il\/index.php\/wp-json\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/rssfeedtelegrambot.bnaya.co.il\/index.php\/wp-json\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/rssfeedtelegrambot.bnaya.co.il\/index.php\/wp-json\/wp\/v2\/users\/1"}],"replies":[{"embeddable":true,"href":"https:\/\/rssfeedtelegrambot.bnaya.co.il\/index.php\/wp-json\/wp\/v2\/comments?post=3777"}],"version-history":[{"count":0,"href":"https:\/\/rssfeedtelegrambot.bnaya.co.il\/index.php\/wp-json\/wp\/v2\/posts\/3777\/revisions"}],"wp:featuredmedia":[{"embeddable":true,"href":"https:\/\/rssfeedtelegrambot.bnaya.co.il\/index.php\/wp-json\/wp\/v2\/media\/94"}],"wp:attachment":[{"href":"https:\/\/rssfeedtelegrambot.bnaya.co.il\/index.php\/wp-json\/wp\/v2\/media?parent=3777"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/rssfeedtelegrambot.bnaya.co.il\/index.php\/wp-json\/wp\/v2\/categories?post=3777"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/rssfeedtelegrambot.bnaya.co.il\/index.php\/wp-json\/wp\/v2\/tags?post=3777"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}