diff --git a/docfx.json b/docfx.json
index 81e6317284893..1b0777c2a43f6 100644
--- a/docfx.json
+++ b/docfx.json
@@ -55,7 +55,8 @@
"csharp-12.0/*.md",
"csharp-13.0/*.md",
"csharp-14.0/*.md",
- "collection-expression-arguments.md"
+ "collection-expression-arguments.md",
+ "unions.md"
],
"src": "_csharplang/proposals",
"dest": "csharp/language-reference/proposals",
@@ -698,6 +699,7 @@
"_csharplang/proposals/csharp-14.0/extension-operators.md": "Extension operators",
"_csharplang/proposals/csharp-14.0/optional-and-named-parameters-in-expression-trees.md": "Optional and named parameters in expression trees",
"_csharplang/proposals/collection-expression-arguments.md": "Collection expression arguments",
+ "_csharplang/proposals/unions.md": "Unions",
"_roslyn/docs/compilers/CSharp/Compiler Breaking Changes - DotNet 7.md": "C# compiler breaking changes since C# 10",
"_roslyn/docs/compilers/CSharp/Compiler Breaking Changes - DotNet 8.md": "C# compiler breaking changes since C# 11",
"_roslyn/docs/compilers/CSharp/Compiler Breaking Changes - DotNet 9.md": "C# compiler breaking changes since C# 12",
@@ -826,6 +828,7 @@
"_csharplang/proposals/csharp-14.0/extension-operators.md": "This proposal extends the proposal for extensions to include *extension operators*, where an operator can be an extension member.",
"_csharplang/proposals/csharp-14.0/optional-and-named-parameters-in-expression-trees.md": "This proposal allows an expression tree to include named and optional parameters. This enables expression trees to be more flexible in how they are constructed.",
"_csharplang/proposals/collection-expression-arguments.md": "This proposal introduces collection expression arguments.",
+ "_csharplang/proposals/unions.md": "This proposal describes union types and union declarations. Unions allow expressing values from a closed set of types with exhaustive pattern matching.",
"_roslyn/docs/compilers/CSharp/Compiler Breaking Changes - DotNet 7.md": "Learn about any breaking changes since the initial release of C# 10 and included in C# 11",
"_roslyn/docs/compilers/CSharp/Compiler Breaking Changes - DotNet 8.md": "Learn about any breaking changes since the initial release of C# 11 and included in C# 12",
"_roslyn/docs/compilers/CSharp/Compiler Breaking Changes - DotNet 9.md": "Learn about any breaking changes since the initial release of C# 12 and included in C# 13",
diff --git a/docs/csharp/language-reference/builtin-types/snippets/unions/BasicUnion.cs b/docs/csharp/language-reference/builtin-types/snippets/unions/BasicUnion.cs
new file mode 100644
index 0000000000000..8ed54f5ab38c7
--- /dev/null
+++ b/docs/csharp/language-reference/builtin-types/snippets/unions/BasicUnion.cs
@@ -0,0 +1,44 @@
+//
+public record class Cat(string Name);
+public record class Dog(string Name);
+public record class Bird(string Name);
+//
+
+//
+public union Pet(Cat, Dog, Bird);
+//
+
+public static class BasicUnionScenario
+{
+ public static void Run()
+ {
+ BasicConversion();
+ PatternMatching();
+ }
+
+ //
+ static void BasicConversion()
+ {
+ Pet pet = new Dog("Rex");
+ Console.WriteLine(pet.Value); // output: Dog { Name = Rex }
+
+ Pet pet2 = new Cat("Whiskers");
+ Console.WriteLine(pet2.Value); // output: Cat { Name = Whiskers }
+ }
+ //
+
+ //
+ static void PatternMatching()
+ {
+ Pet pet = new Dog("Rex");
+
+ var name = pet switch
+ {
+ Dog d => d.Name,
+ Cat c => c.Name,
+ Bird b => b.Name,
+ };
+ Console.WriteLine(name); // output: Rex
+ }
+ //
+}
diff --git a/docs/csharp/language-reference/builtin-types/snippets/unions/BodyMembers.cs b/docs/csharp/language-reference/builtin-types/snippets/unions/BodyMembers.cs
new file mode 100644
index 0000000000000..8ce8467dec5e0
--- /dev/null
+++ b/docs/csharp/language-reference/builtin-types/snippets/unions/BodyMembers.cs
@@ -0,0 +1,30 @@
+//
+public union OneOrMore(T, IEnumerable)
+{
+ public IEnumerable AsEnumerable() => Value switch
+ {
+ T single => [single],
+ IEnumerable multiple => multiple,
+ _ => []
+ };
+}
+//
+
+public static class BodyMembersScenario
+{
+ public static void Run()
+ {
+ BodyMembersExample();
+ }
+
+ //
+ static void BodyMembersExample()
+ {
+ OneOrMore single = "hello";
+ OneOrMore multiple = new[] { "a", "b", "c" }.AsEnumerable();
+
+ Console.WriteLine(string.Join(", ", single.AsEnumerable())); // output: hello
+ Console.WriteLine(string.Join(", ", multiple.AsEnumerable())); // output: a, b, c
+ }
+ //
+}
diff --git a/docs/csharp/language-reference/builtin-types/snippets/unions/ClassUnion.cs b/docs/csharp/language-reference/builtin-types/snippets/unions/ClassUnion.cs
new file mode 100644
index 0000000000000..5ae6b433eeaf3
--- /dev/null
+++ b/docs/csharp/language-reference/builtin-types/snippets/unions/ClassUnion.cs
@@ -0,0 +1,38 @@
+//
+[System.Runtime.CompilerServices.Union]
+public class Result : System.Runtime.CompilerServices.IUnion
+{
+ private readonly object? _value;
+
+ public Result(T? value) { _value = value; }
+ public Result(Exception? value) { _value = value; }
+
+ public object? Value => _value;
+}
+//
+
+public static class ClassUnionScenario
+{
+ public static void Run()
+ {
+ ClassUnionExample();
+ }
+
+ //
+ static void ClassUnionExample()
+ {
+ Result ok = new Result("success");
+ Result err = new Result(new InvalidOperationException("failed"));
+
+ Console.WriteLine(Describe(ok)); // output: OK: success
+ Console.WriteLine(Describe(err)); // output: Error: failed
+
+ static string Describe(Result result) => result switch
+ {
+ string s => $"OK: {s}",
+ Exception e => $"Error: {e.Message}",
+ null => "null",
+ };
+ }
+ //
+}
diff --git a/docs/csharp/language-reference/builtin-types/snippets/unions/GenericUnion.cs b/docs/csharp/language-reference/builtin-types/snippets/unions/GenericUnion.cs
new file mode 100644
index 0000000000000..9593fd399e6c1
--- /dev/null
+++ b/docs/csharp/language-reference/builtin-types/snippets/unions/GenericUnion.cs
@@ -0,0 +1,35 @@
+//
+public record class None;
+public record class Some(T Value);
+public union Option(None, Some);
+//
+
+public static class GenericUnionScenario
+{
+ public static void Run()
+ {
+ GenericUnionExample();
+ }
+
+ //
+ static void GenericUnionExample()
+ {
+ Option some = new Some(42);
+ Option none = new None();
+
+ var result = some switch
+ {
+ Some s => $"Has value: {s.Value}",
+ None => "No value",
+ };
+ Console.WriteLine(result); // output: Has value: 42
+
+ var result2 = none switch
+ {
+ Some s => $"Has value: {s.Value}",
+ None => "No value",
+ };
+ Console.WriteLine(result2); // output: No value
+ }
+ //
+}
diff --git a/docs/csharp/language-reference/builtin-types/snippets/unions/ManualUnion.cs b/docs/csharp/language-reference/builtin-types/snippets/unions/ManualUnion.cs
new file mode 100644
index 0000000000000..b4055e06cf222
--- /dev/null
+++ b/docs/csharp/language-reference/builtin-types/snippets/unions/ManualUnion.cs
@@ -0,0 +1,37 @@
+//
+[System.Runtime.CompilerServices.Union]
+public struct Shape : System.Runtime.CompilerServices.IUnion
+{
+ private readonly object? _value;
+
+ public Shape(Circle value) { _value = value; }
+ public Shape(Rectangle value) { _value = value; }
+
+ public object? Value => _value;
+}
+
+public record class Circle(double Radius);
+public record class Rectangle(double Width, double Height);
+//
+
+public static class ManualUnionScenario
+{
+ public static void Run()
+ {
+ ManualUnionExample();
+ }
+
+ //
+ static void ManualUnionExample()
+ {
+ Shape shape = new Shape(new Circle(5.0));
+
+ var area = shape switch
+ {
+ Circle c => Math.PI * c.Radius * c.Radius,
+ Rectangle r => r.Width * r.Height,
+ };
+ Console.WriteLine($"{area:F2}"); // output: 78.54
+ }
+ //
+}
diff --git a/docs/csharp/language-reference/builtin-types/snippets/unions/MemberProvider.cs b/docs/csharp/language-reference/builtin-types/snippets/unions/MemberProvider.cs
new file mode 100644
index 0000000000000..5505875ebccaa
--- /dev/null
+++ b/docs/csharp/language-reference/builtin-types/snippets/unions/MemberProvider.cs
@@ -0,0 +1,36 @@
+// Uncomment when union member providers are available in the compiler:
+
+//
+// [System.Runtime.CompilerServices.Union]
+// public record class Outcome : Outcome.IUnionMembers
+// {
+// private readonly object? _value;
+//
+// private Outcome(object? value) => _value = value;
+//
+// public interface IUnionMembers
+// {
+// static Outcome Create(T? value) => new(value);
+// static Outcome Create(Exception? value) => new(value);
+// object? Value { get; }
+// }
+//
+// object? IUnionMembers.Value => _value;
+// }
+//
+
+//
+// public static class MemberProviderScenario
+// {
+// public static void Run()
+// {
+// Outcome ok = "success";
+// var msg = ok switch
+// {
+// string s => $"OK: {s}",
+// Exception e => $"Error: {e.Message}",
+// };
+// Console.WriteLine(msg);
+// }
+// }
+//
diff --git a/docs/csharp/language-reference/builtin-types/snippets/unions/NonBoxingAccess.cs b/docs/csharp/language-reference/builtin-types/snippets/unions/NonBoxingAccess.cs
new file mode 100644
index 0000000000000..a0b3d60663a99
--- /dev/null
+++ b/docs/csharp/language-reference/builtin-types/snippets/unions/NonBoxingAccess.cs
@@ -0,0 +1,70 @@
+//
+[System.Runtime.CompilerServices.Union]
+public struct IntOrBool : System.Runtime.CompilerServices.IUnion
+{
+ private readonly int _intValue;
+ private readonly bool _boolValue;
+ private readonly byte _tag; // 0 = none, 1 = int, 2 = bool
+
+ public IntOrBool(int? value)
+ {
+ if (value.HasValue)
+ {
+ _intValue = value.Value;
+ _tag = 1;
+ }
+ }
+
+ public IntOrBool(bool? value)
+ {
+ if (value.HasValue)
+ {
+ _boolValue = value.Value;
+ _tag = 2;
+ }
+ }
+
+ public object? Value => _tag switch
+ {
+ 1 => _intValue,
+ 2 => _boolValue,
+ _ => null
+ };
+
+ public bool HasValue => _tag != 0;
+
+ public bool TryGetValue(out int value)
+ {
+ value = _intValue;
+ return _tag == 1;
+ }
+
+ public bool TryGetValue(out bool value)
+ {
+ value = _boolValue;
+ return _tag == 2;
+ }
+}
+//
+
+public static class NonBoxingAccessScenario
+{
+ public static void Run()
+ {
+ NonBoxingExample();
+ }
+
+ //
+ static void NonBoxingExample()
+ {
+ IntOrBool val = new IntOrBool((int?)42);
+
+ var description = val switch
+ {
+ int i => $"int: {i}",
+ bool b => $"bool: {b}",
+ };
+ Console.WriteLine(description); // output: int: 42
+ }
+ //
+}
diff --git a/docs/csharp/language-reference/builtin-types/snippets/unions/NullHandling.cs b/docs/csharp/language-reference/builtin-types/snippets/unions/NullHandling.cs
new file mode 100644
index 0000000000000..67e1fb8950359
--- /dev/null
+++ b/docs/csharp/language-reference/builtin-types/snippets/unions/NullHandling.cs
@@ -0,0 +1,44 @@
+public static class NullHandlingScenario
+{
+ public static void Run()
+ {
+ NullHandling();
+ NullableUnionExample();
+ }
+
+ //
+ static void NullHandling()
+ {
+ Pet pet = default;
+ Console.WriteLine(pet.Value is null); // output: True
+
+ var description = pet switch
+ {
+ Dog d => d.Name,
+ Cat c => c.Name,
+ Bird b => b.Name,
+ null => "no pet",
+ };
+ Console.WriteLine(description); // output: no pet
+ }
+ //
+
+ //
+ static void NullableUnionExample()
+ {
+ Pet? maybePet = new Dog("Buddy");
+ Pet? noPet = null;
+
+ Console.WriteLine(Describe(maybePet)); // output: Dog: Buddy
+ Console.WriteLine(Describe(noPet)); // output: no pet
+
+ static string Describe(Pet? pet) => pet switch
+ {
+ Dog d => d.Name,
+ Cat c => c.Name,
+ Bird b => b.Name,
+ null => "no pet",
+ };
+ }
+ //
+}
diff --git a/docs/csharp/language-reference/builtin-types/snippets/unions/Program.cs b/docs/csharp/language-reference/builtin-types/snippets/unions/Program.cs
new file mode 100644
index 0000000000000..eca889a8175e5
--- /dev/null
+++ b/docs/csharp/language-reference/builtin-types/snippets/unions/Program.cs
@@ -0,0 +1,10 @@
+BasicUnionScenario.Run();
+GenericUnionScenario.Run();
+ValueTypeCasesScenario.Run();
+BodyMembersScenario.Run();
+NullHandlingScenario.Run();
+ManualUnionScenario.Run();
+NonBoxingAccessScenario.Run();
+ClassUnionScenario.Run();
+// Uncomment when union member providers are available in the compiler:
+// MemberProviderScenario.Run();
diff --git a/docs/csharp/language-reference/builtin-types/snippets/unions/RuntimePolyfill.cs b/docs/csharp/language-reference/builtin-types/snippets/unions/RuntimePolyfill.cs
new file mode 100644
index 0000000000000..52dcc5501d328
--- /dev/null
+++ b/docs/csharp/language-reference/builtin-types/snippets/unions/RuntimePolyfill.cs
@@ -0,0 +1,13 @@
+// Remove this file when UnionAttribute and IUnion are included in the .NET runtime.
+//
+namespace System.Runtime.CompilerServices
+{
+ [AttributeUsage(AttributeTargets.Class | AttributeTargets.Struct, AllowMultiple = false)]
+ public sealed class UnionAttribute : Attribute;
+
+ public interface IUnion
+ {
+ object? Value { get; }
+ }
+}
+//
diff --git a/docs/csharp/language-reference/builtin-types/snippets/unions/ValueTypeCases.cs b/docs/csharp/language-reference/builtin-types/snippets/unions/ValueTypeCases.cs
new file mode 100644
index 0000000000000..5c9349e1e608d
--- /dev/null
+++ b/docs/csharp/language-reference/builtin-types/snippets/unions/ValueTypeCases.cs
@@ -0,0 +1,29 @@
+//
+public union IntOrString(int, string);
+//
+
+public static class ValueTypeCasesScenario
+{
+ public static void Run()
+ {
+ ValueTypeCasesExample();
+ }
+
+ //
+ static void ValueTypeCasesExample()
+ {
+ IntOrString val1 = 42;
+ IntOrString val2 = "hello";
+
+ Console.WriteLine(Describe(val1)); // output: int: 42
+ Console.WriteLine(Describe(val2)); // output: string: hello
+
+ static string Describe(IntOrString value) => value switch
+ {
+ int i => $"int: {i}",
+ string s => $"string: {s}",
+ null => "null",
+ };
+ }
+ //
+}
diff --git a/docs/csharp/language-reference/builtin-types/snippets/unions/unions.csproj b/docs/csharp/language-reference/builtin-types/snippets/unions/unions.csproj
new file mode 100644
index 0000000000000..c2cb17a624618
--- /dev/null
+++ b/docs/csharp/language-reference/builtin-types/snippets/unions/unions.csproj
@@ -0,0 +1,11 @@
+
+
+
+ Exe
+ net11.0
+ preview
+ enable
+ enable
+
+
+
diff --git a/docs/csharp/language-reference/builtin-types/union.md b/docs/csharp/language-reference/builtin-types/union.md
new file mode 100644
index 0000000000000..75b6a9d10c512
--- /dev/null
+++ b/docs/csharp/language-reference/builtin-types/union.md
@@ -0,0 +1,235 @@
+---
+title: "Union types"
+description: Learn about union types in C#. Unions express values from a closed set of types with exhaustive pattern matching support.
+ms.date: 03/20/2026
+f1_keywords:
+ - "union_CSharpKeyword"
+helpviewer_keywords:
+ - "union keyword [C#]"
+ - "union type [C#]"
+ - "case type [C#]"
+ai-usage: ai-assisted
+---
+# Union types (C# reference)
+
+A *union type* represents a value that can be one of several *case types*. Unions provide implicit conversions from each case type, exhaustive pattern matching, and enhanced nullability tracking. Use the `union` keyword to declare a union type:
+
+:::code language="csharp" source="snippets/unions/BasicUnion.cs" id="BasicDeclaration":::
+
+This declaration creates a `Pet` union with three case types: `Cat`, `Dog`, and `Bird`. You can assign any case type value to a `Pet` variable, and the compiler ensures that `switch` expressions cover all case types.
+
+Declare a union when a value must be exactly one of a fixed set of types and you want the compiler to enforce that every possibility is handled. Common scenarios include:
+
+- **Result-or-error returns**: A method returns either a success value or an error value, and the caller must handle both. A union like `union Result(Success, Error)` makes the set of outcomes explicit.
+- **Message or command dispatching**: A system processes a closed set of message types. A union ensures new message types produce compile-time warnings at every `switch` that doesn't handle them yet.
+- **Replacing marker interfaces or abstract base classes**: If you use an interface or abstract class solely to group types for pattern matching, a union gives you exhaustiveness checking without requiring inheritance or shared members.
+
+A union differs from other type declarations in important ways:
+
+- Unlike a `class` or `struct`, a union doesn't define new data members. Instead, it composes existing types into a closed set of alternatives.
+- Unlike an `interface`, a union is closed—you define the complete list of case types in the declaration, and the compiler uses that list for exhaustiveness checks.
+- Unlike a `record`, a union doesn't add equality, cloning, or deconstruction behavior. A union focuses on "which case is it?" rather than "what fields does it have?"
+
+[!INCLUDE[csharp-version-note](../includes/initial-version.md)]
+
+> [!IMPORTANT]
+> In .NET 10 Preview 2, the runtime doesn't include the `UnionAttribute` and `IUnion` interface. To use union types, you must declare them yourself. To see the required declarations, see [Union implementation](#union-implementation).
+
+## Union declarations
+
+A union declaration specifies a name and a list of case types:
+
+```csharp
+public union Pet(Cat, Dog, Bird);
+```
+
+*Case types* can be any type that converts to `object`, including classes, structs, interfaces, type parameters, nullable types, and other unions. The following examples show different case type possibilities:
+
+:::code language="csharp" source="snippets/unions/BasicUnion.cs" id="CaseTypes":::
+:::code language="csharp" source="snippets/unions/GenericUnion.cs" id="GenericUnion":::
+:::code language="csharp" source="snippets/unions/ValueTypeCases.cs" id="ValueTypeCases":::
+
+When a case type is a value type (like `int`), the value is boxed when stored in the union's `Value` property. Unions store their contents as a single `object?` reference.
+
+A union declaration can include a body with additional members, just like a struct, subject to some restrictions:
+
+:::code language="csharp" source="snippets/unions/BodyMembers.cs" id="BodyMembers":::
+
+Union declarations can't include instance fields, auto-properties, or field-like events. You also can't declare public constructors with a single parameter, because the compiler generates those constructors as union creation members.
+
+## Union conversions
+
+An implicit *union conversion* exists from each case type to the union type. You don't need to call a constructor explicitly:
+
+:::code language="csharp" source="snippets/unions/BasicUnion.cs" id="BasicConversion":::
+
+Union conversions work by calling the corresponding generated constructor. If a user-defined implicit conversion operator exists for the same type, the user-defined operator takes priority over the union conversion. See the [language specification](~/_csharplang/proposals/unions.md) for details on conversion priority.
+
+A union conversion to a nullable union struct (`T?`) also works when `T` is a union type:
+
+:::code language="csharp" source="snippets/unions/NullHandling.cs" id="NullableUnionExample":::
+
+## Union matching
+
+When you pattern match on a union type, patterns apply to the union's `Value` property, not the union value itself. This "unwrapping" behavior means the union is transparent to pattern matching:
+
+:::code language="csharp" source="snippets/unions/BasicUnion.cs" id="PatternMatching":::
+
+Two patterns are exceptions to this rule: the `var` pattern and the discard `_` pattern apply to the union value itself, not its `Value` property. Use `var` to capture the union value when `GetPet()` returns a `Pet?` (`Nullable`):
+
+```csharp
+if (GetPet() is var pet) { /* pet is the Pet? value returned from GetPet */ }
+```
+
+In logical patterns, each branch follows the unwrapping rule individually. The following pattern tests that the `Pet?` isn't null *and* its `Value` isn't null:
+
+```csharp
+GetPet() switch
+{
+ var pet and not null => ..., // 'var pet' captures the Pet?; 'not null' checks Value
+}
+```
+
+> [!NOTE]
+> Because patterns apply to `Value`, a pattern like `pet is Pet` typically doesn't match, since `Pet` is tested against the *contents* of the union, not the union itself.
+
+### Null matching
+
+For struct unions, the `null` pattern checks whether `Value` is null:
+
+:::code language="csharp" source="snippets/unions/NullHandling.cs" id="NullHandling":::
+
+For class-based unions, `null` succeeds when either the union reference itself is null or its `Value` property is null:
+
+```csharp
+Result? result = null;
+if (result is null) { /* true — the reference is null */ }
+
+Result empty = new Result((string?)null);
+if (empty is null) { /* true — Value is null */ }
+```
+
+For nullable union struct types (`Pet?`), `null` succeeds when the nullable wrapper has no value or when the underlying union's `Value` is null.
+
+## Union exhaustiveness
+
+A `switch` expression is exhaustive when it handles all case types of a union. The compiler warns only if a case type isn't handled. You don't need to include a discard pattern (`_`) or `var` pattern to match any type:
+
+:::code language="csharp" source="snippets/unions/BasicUnion.cs" id="PatternMatching":::
+
+If the null state of the union's `Value` property is "maybe null," you must also handle `null` to avoid a warning:
+
+:::code language="csharp" source="snippets/unions/NullHandling.cs" id="NullHandling":::
+
+## Nullability
+
+The compiler tracks the null state of a union's `Value` property through the following rules:
+
+- When you create a union value from a case type (through a constructor or union conversion), `Value` gets the null state of the incoming value.
+- When the non-boxing access pattern's `HasValue` or `TryGetValue(...)` members query the union's contents, the null state of `Value` becomes "not null" on the `true` branch.
+
+## Custom union types
+
+The compiler lowers a `union` declaration to a `struct` marked with the `[System.Runtime.CompilerServices.Union]` attribute, implements the `IUnion` interface, and generates a public constructor and an implicit conversion for each case type along with a `Value` property. That generated form is opinionated. It's always a struct, always boxes value-type cases, and always stores contents as `object?`.
+
+When you need different behavior - such as a class-based union, a custom storage strategy, interop support, or if you want to adapt an existing type - you can create a union type manually.
+
+Any class or struct with a `[Union]` attribute is a *union type* if it follows the *basic union pattern*. The basic union pattern requires:
+
+- A `[System.Runtime.CompilerServices.Union]` attribute on the type.
+- One or more public constructors, each with a single by-value or `in` parameter. The parameter type of each constructor defines a *case type*.
+- A public `Value` property of type `object?` (or `object`) with a `get` accessor.
+
+All union members must be public. The compiler uses these members to implement union conversions, pattern matching, and exhaustiveness checks. You can also implement the [Non-boxing access pattern](#non-boxing-access-pattern) or create a [class-based union type](#class-based-union-types).
+
+The compiler assumes that custom union types satisfy these behavioral rules:
+
+- **Soundness**: `Value` always returns `null` or a value of one of the case types - never a value of a different type. For struct unions, `default` produces a `Value` of `null`.
+- **Stability**: If you create a union value from a case type, `Value` matches that case type (or is `null` if the input was `null`).
+- **Creation equivalence**: If a value is implicitly convertible to two different case types, both creation members produce the same observable behavior.
+- **Access pattern consistency**: The `HasValue` and `TryGetValue` members, if present, behave equivalently to checking `Value` directly.
+
+The following example shows a custom union type:
+
+:::code language="csharp" source="snippets/unions/ManualUnion.cs" id="ManualBasicPattern":::
+
+:::code language="csharp" source="snippets/unions/ManualUnion.cs" id="ManualUnionExample":::
+
+### Non-boxing access pattern
+
+A custom union type can optionally implement the *non-boxing access pattern* to enable strongly typed access to value-type cases without boxing during pattern matching. This pattern requires:
+
+- A `HasValue` property of type `bool` that returns `true` when `Value` isn't `null`.
+- A `TryGetValue` method for each case type that returns `bool` and delivers the value through an `out` parameter.
+
+:::code language="csharp" source="snippets/unions/NonBoxingAccess.cs" id="NonBoxingAccessPattern":::
+
+:::code language="csharp" source="snippets/unions/NonBoxingAccess.cs" id="NonBoxingExample":::
+
+The compiler prefers `TryGetValue` over the `Value` property when implementing pattern matching, which avoids boxing value types.
+
+
+
+
+### Class-based union types
+
+A class can also be a union type. This type of union is useful when you need reference semantics or inheritance:
+
+:::code language="csharp" source="snippets/unions/ClassUnion.cs" id="ClassUnion":::
+
+:::code language="csharp" source="snippets/unions/ClassUnion.cs" id="ClassUnionExample":::
+
+For class-based unions, the `null` pattern matches both a null reference and a null `Value`.
+
+## Union implementation
+
+The following attribute and interface support union types at compile time and runtime:
+
+:::code language="csharp" source="snippets/unions/RuntimePolyfill.cs" id="RuntimePolyfill":::
+
+Union declarations generated by the compiler implement `IUnion`. You can check for any union value at runtime by using `IUnion`:
+
+```csharp
+if (value is IUnion { Value: null }) { /* the union's value is null */ }
+```
+
+When you declare a `union` type, the compiler generates a struct that implements `IUnion`. For example, the `Pet` declaration:
+
+```csharp
+public union Pet(Cat, Dog, Bird);
+```
+
+becomes equivalent to:
+
+```csharp
+[Union] public struct Pet : IUnion
+{
+ public Pet(Cat value) => Value = value;
+ public Pet(Dog value) => Value = value;
+ public Pet(Bird value) => Value = value;
+ public object? Value { get; }
+}
+```
+
+> [!IMPORTANT]
+> In .NET 10 Preview 2, these types aren't included in the runtime. You must declare them in your project to use union types. They'll be included in a future .NET preview.
+
+## C# language specification
+
+For more information, see the [Unions](~/_csharplang/proposals/unions.md) feature specification.
+
+## See also
+
+- [The C# type system](../../fundamentals/types/index.md)
+- [Pattern matching](../operators/patterns.md)
+- [Switch expression](../operators/switch-expression.md)
+- [Value types](value-types.md)
+- [Records](record.md)
diff --git a/docs/csharp/language-reference/builtin-types/value-types.md b/docs/csharp/language-reference/builtin-types/value-types.md
index e6d03c3df25ab..f7ca55c6794fd 100644
--- a/docs/csharp/language-reference/builtin-types/value-types.md
+++ b/docs/csharp/language-reference/builtin-types/value-types.md
@@ -1,7 +1,7 @@
---
description: Value types vs reference types, kinds of value types, and the built-in value types in C#
title: "Value types"
-ms.date: 01/14/2026
+ms.date: 03/20/2026
f1_keywords:
- "cs.valuetypes"
helpviewer_keywords:
@@ -11,13 +11,13 @@ helpviewer_keywords:
---
# Value types (C# reference)
-*Value types* and [reference types](../keywords/reference-types.md) are the two main categories of C# types. A variable of a value type contains an instance of the type. This behavior differs from a variable of a reference type, which contains a reference to an instance of the type. By default, on [assignment](../operators/assignment-operator.md), passing an argument to a method, and returning a method result, variable values are copied. In the case of value-type variables, the corresponding type instances are copied. The following example demonstrates that behavior:
+*Value types* and [reference types](../keywords/reference-types.md) are the two main categories of C# types. A variable of a value type contains an instance of the type. This behavior differs from a variable of a reference type, which contains a reference to an instance of the type. By default, on [assignment](../operators/assignment-operator.md), passing an argument to a method, and returning a method result, you copy variable values. In the case of value-type variables, you copy the corresponding type instances. The following example demonstrates that behavior:
:::code language="csharp" source="snippets/shared/ValueTypes.cs" id="ValueTypeCopied":::
As the preceding example shows, operations on a value-type variable affect only that instance of the value type, stored in the variable.
-If a value type contains a data member of a reference type, only the reference to the instance of the reference type is copied when a value-type instance is copied. Both the copy and original value-type instance have access to the same reference-type instance. The following example demonstrates that behavior:
+If a value type contains a data member of a reference type, you copy only the reference to the instance of the reference type when you copy a value-type instance. Both the copy and original value-type instance have access to the same reference-type instance. The following example demonstrates that behavior:
:::code language="csharp" source="snippets/shared/ValueTypes.cs" id="ShallowCopy":::
@@ -26,14 +26,15 @@ If a value type contains a data member of a reference type, only the reference t
## Kinds of value types and type constraints
-A value type can be one of the two following kinds:
+A value type can be one of the following kinds:
- a [structure type](struct.md), which encapsulates data and related functionality
- an [enumeration type](enum.md), which is defined by a set of named constants and represents a choice or a combination of choices
+- a [union declaration](union.md), which defines a closed set of case types that a value can represent
A [nullable value type](nullable-value-types.md) `T?` represents all values of its underlying value type `T` and an additional [null](../keywords/null.md) value. You can't assign `null` to a variable of a value type, unless it's a nullable value type.
-You can use the [`struct` constraint](../../programming-guide/generics/constraints-on-type-parameters.md) to specify that a type parameter is a non-nullable value type. Both structure and enumeration types satisfy the `struct` constraint. You can use `System.Enum` in a base class constraint (that is known as the [enum constraint](../../programming-guide/generics/constraints-on-type-parameters.md#enum-constraints)) to specify that a type parameter is an enumeration type.
+Use the [`struct` constraint](../../programming-guide/generics/constraints-on-type-parameters.md) to specify that a type parameter is a non-nullable value type. Both structure and enumeration types satisfy the `struct` constraint. Use `System.Enum` in a base class constraint (that is known as the [enum constraint](../../programming-guide/generics/constraints-on-type-parameters.md#enum-constraints)) to specify that a type parameter is an enumeration type.
## Built-in value types
@@ -44,14 +45,14 @@ C# provides the following built-in value types, also known as *simple types*:
- [bool](bool.md) that represents a Boolean value
- [char](char.md) that represents a Unicode UTF-16 character
-All simple types are structure types. They differ from other structure types in that they permit certain additional operations:
+All simple types are struct types. They differ from other struct types in that they permit certain additional operations:
- You can use literals to provide a value of a simple type.
For example, `'A'` is a literal of the type `char`, `2001` is a literal of the type `int`, and `12.34m` is a literal of the type `decimal`.
- You can declare constants of the simple types by using the [const](../keywords/const.md) keyword.
For example, you can define `const decimal = 12.34m`.
-
You can't declare constants of other structure types.
+
You can't declare constants of other struct types.
- Constant expressions, whose operands are all constants of the simple types, are evaluated at compile time.
diff --git a/docs/csharp/language-reference/operators/patterns.md b/docs/csharp/language-reference/operators/patterns.md
index b753c3f9d24f7..873b5872c12a9 100644
--- a/docs/csharp/language-reference/operators/patterns.md
+++ b/docs/csharp/language-reference/operators/patterns.md
@@ -1,7 +1,7 @@
---
title: "Patterns - Pattern matching using the is and switch expressions."
description: "Learn about the patterns supported by the `is` and `switch` expressions. Combine multiple patterns using the `and`, `or`, and `not` operators."
-ms.date: 01/20/2026
+ms.date: 03/20/2026
f1_keywords:
- "and_CSharpKeyword"
- "or_CSharpKeyword"
@@ -43,7 +43,7 @@ For an example of how to use those patterns to build a data-driven algorithm, se
## Declaration and type patterns
-Use declaration and type patterns to check if the run-time type of an expression is compatible with a given type. With a declaration pattern, you can also declare a new local variable. When a declaration pattern matches an expression, the variable is assigned to the converted expression result, as the following example shows:
+Use declaration and type patterns to check if the run-time type of an expression is compatible with a given type. By using a declaration pattern, you can also declare a new local variable. When a declaration pattern matches an expression, it assigns the variable to the converted expression result, as the following example shows:
:::code language="csharp" source="snippets/patterns/DeclarationAndTypePatterns.cs" id="BasicExample":::
@@ -101,7 +101,7 @@ Use a constant pattern to check for `null`, as the following example shows:
:::code language="csharp" source="snippets/patterns/ConstantPattern.cs" id="NullCheck":::
-The compiler guarantees that no user-overloaded equality operator `==` is invoked when expression `x is null` is evaluated.
+The compiler guarantees that it doesn't invoke a user-overloaded equality operator `==` when it evaluates expression `x is null`.
You can use a [negated](#logical-patterns) `null` constant pattern to check for non-null, as the following example shows:
@@ -115,13 +115,13 @@ Use a *relational pattern* to compare an expression result with a constant, as t
:::code language="csharp" source="snippets/patterns/RelationalPatterns.cs" id="BasicExample":::
-In a relational pattern, you can use any of the [relational operators](comparison-operators.md) `<`, `>`, `<=`, or `>=`. The right-hand part of a relational pattern must be a constant expression. The constant expression can be of an [integer](../builtin-types/integral-numeric-types.md), [floating-point](../builtin-types/floating-point-numeric-types.md), [char](../builtin-types/char.md), or [enum](../builtin-types/enum.md) type.
+In a relational pattern, use any of the [relational operators](comparison-operators.md) `<`, `>`, `<=`, or `>=`. The right-hand part of a relational pattern must be a constant expression. The constant expression can be of an [integer](../builtin-types/integral-numeric-types.md), [floating-point](../builtin-types/floating-point-numeric-types.md), [char](../builtin-types/char.md), or [enum](../builtin-types/enum.md) type.
To check if an expression result is in a certain range, match it against a [conjunctive `and` pattern](#logical-patterns), as the following example shows:
:::code language="csharp" source="snippets/patterns/RelationalPatterns.cs" id="WithCombinators":::
-If an expression result is `null` or fails to convert to the type of a constant by a nullable or unboxing conversion, a relational pattern doesn't match an expression.
+If an expression result is `null` or fails to convert to the type of a constant by using a nullable or unboxing conversion, a relational pattern doesn't match the expression.
For more information, see the [Relational patterns](~/_csharplang/proposals/csharp-9.0/patterns3.md#relational-patterns) section of the feature proposal note.
@@ -141,17 +141,17 @@ Use the `not`, `and`, and `or` pattern combinators to create the following *logi
:::code language="csharp" source="snippets/patterns/LogicalPatterns.cs" id="OrPattern":::
-As the preceding example shows, you can repeatedly use the pattern combinators in a pattern.
+As the preceding example shows, you can use the pattern combinators repeatedly in a pattern.
### Precedence and order of checking
-The pattern combinators follow this order, based on the binding order of expressions:
+The pattern combinators check expressions in this order, based on the binding order of expressions:
- `not`
- `and`
- `or`
-The `not` pattern binds to its operand first. The `and` pattern binds after any `not` pattern expression binding. The `or` pattern binds after all `not` and `and` patterns bind to operands. The following example tries to match all characters that aren't lowercase letters, `a` - `z`. It has an error, because the `not` pattern binds before the `and` pattern:
+The `not` pattern binds to its operand first. The `and` pattern binds after any `not` pattern expression binding. The `or` pattern binds after all `not` and `and` patterns bind to operands. The following example tries to match all characters that aren't lowercase letters, `a` through `z`. It has an error, because the `not` pattern binds before the `and` pattern:
:::code language="csharp" source="snippets/patterns/LogicalPatterns.cs" id="NegationWithoutParens":::
@@ -168,7 +168,7 @@ Adding parentheses becomes more important as your patterns become more complicat
:::code language="csharp" source="snippets/patterns/LogicalPatterns.cs" id="WithParentheses":::
> [!NOTE]
-> The order in which patterns having the same binding order are checked is undefined. At run time, the right-hand nested patterns of multiple `or` patterns and multiple `and` patterns can be checked first.
+> The order in which the compiler checks patterns that have the same binding order is undefined. At run time, the compiler can check the right-hand nested patterns of multiple `or` patterns and multiple `and` patterns first.
For more information, see the [Pattern combinators](~/_csharplang/proposals/csharp-9.0/patterns3.md#pattern-combinators) section of the feature proposal note.
@@ -236,7 +236,7 @@ You can also extend a positional pattern in any of the following ways:
:::code language="csharp" source="snippets/patterns/PositionalPattern.cs" id="WithPropertyPattern":::
-- Combine two preceding usages, as the following example shows:
+- Combine the two preceding usages, as the following example shows:
:::code language="csharp" source="snippets/patterns/PositionalPattern.cs" id="CompletePositionalPattern":::
@@ -266,7 +266,7 @@ Use a *discard pattern* `_` to match any expression, including `null`, as the fo
:::code language="csharp" source="snippets/patterns/DiscardPattern.cs" id="BasicExample":::
-In the preceding example, a discard pattern handles `null` and any integer value that doesn't have the corresponding member of the enumeration. That guarantee ensures that a `switch` expression in the example handles all possible input values. If you don't use a discard pattern in a `switch` expression and none of the expression's patterns matches an input, the runtime [throws an exception](switch-expression.md#non-exhaustive-switch-expressions). The compiler generates a warning if a `switch` expression doesn't handle all possible input values.
+In the preceding example, a discard pattern handles `null` and any integer value that doesn't have the corresponding member of the enumeration. That guarantee ensures that a `switch` expression in the example handles all possible input values. If you don't use a discard pattern in a `switch` expression and none of the expression's patterns matches an input, the runtime [throws an exception](switch-expression.md#nonexhaustive-switch-expressions). The compiler generates a warning if a `switch` expression doesn't handle all possible input values.
A discard pattern can't be a pattern in an `is` expression or a `switch` statement. In those cases, to match any expression, use a [`var` pattern](#var-pattern) with a discard: `var _`. A discard pattern can be a pattern in a `switch` expression.
@@ -274,7 +274,7 @@ For more information, see the [Discard pattern](~/_csharpstandard/standard/patte
## Parenthesized pattern
-You can put parentheses around any pattern. Typically, you do that to emphasize or change the precedence in [logical patterns](#logical-patterns), as the following example shows:
+You can put parentheses around any pattern. Typically, you do this to emphasize or change the precedence in [logical patterns](#logical-patterns), as the following example shows:
:::code language="csharp" source="snippets/patterns/LogicalPatterns.cs" id="ChangedPrecedence":::
@@ -288,7 +288,7 @@ As the preceding example shows, a list pattern matches when each nested pattern
:::code language="csharp" source="snippets/patterns/ListPattern.cs" id="MatchAnyElement":::
-The preceding examples match a whole input sequence against a list pattern. To match elements only at the start or/and the end of an input sequence, use the *slice pattern* `..`, as the following example shows:
+The preceding examples match a whole input sequence against a list pattern. To match elements only at the start or end - or both - of an input sequence, use the *slice pattern* `..`, as the following example shows:
:::code language="csharp" source="snippets/patterns/ListPattern.cs" id="UseSlice":::
@@ -300,6 +300,30 @@ You can also nest a subpattern within a slice pattern, as the following example
For more information, see the [List patterns](~/_csharplang/proposals/csharp-11.0/list-patterns.md) feature proposal note.
+## Union patterns
+
+Starting with C# 15, when the incoming value of a pattern is a [union type](../builtin-types/union.md), patterns automatically *unwrap* the union. They apply to the union's `Value` property rather than the union value itself. This behavior makes the union transparent to pattern matching:
+
+```csharp
+public record class Cat(string Name);
+public record class Dog(string Name);
+public union Pet(Cat, Dog);
+
+string Describe(Pet pet) => pet switch
+{
+ Dog d => d.Name,
+ Cat c => c.Name,
+};
+```
+
+Two patterns are exceptions: the `var` pattern and the discard `_` pattern apply to the union value itself, not its `Value` property.
+
+The `null` pattern checks whether the union's `Value` is null. For class-based unions, `null` also succeeds when the union reference itself is null.
+
+When a union type provides the *non-boxing access pattern* (`HasValue` and `TryGetValue` members), the compiler uses those members to avoid boxing value-type cases during pattern matching.
+
+For more information, see the [Union matching](../builtin-types/union.md#union-matching) section. For the specification, see [Unions](~/_csharplang/proposals/unions.md).
+
## C# language specification
For more information, see the [Patterns and pattern matching](~/_csharpstandard/standard/patterns.md) section of the [C# language specification](~/_csharpstandard/standard/README.md).
diff --git a/docs/csharp/language-reference/operators/switch-expression.md b/docs/csharp/language-reference/operators/switch-expression.md
index d86dc324090ae..cbd6d489773c8 100644
--- a/docs/csharp/language-reference/operators/switch-expression.md
+++ b/docs/csharp/language-reference/operators/switch-expression.md
@@ -1,7 +1,7 @@
---
title: "switch expression - Evaluate a pattern match expression using the `switch` expression"
description: Learn about the C# `switch` expression that provides switch-like semantics based on pattern matching. You can compute a value based on which pattern an input variable matches.
-ms.date: 01/20/2026
+ms.date: 03/20/2026
f1_keywords:
- "switch-expression_CSharpKeyword"
helpviewer_keywords:
@@ -26,7 +26,7 @@ The preceding example shows the basic elements of a `switch` expression:
In the preceding example, a `switch` expression uses the following patterns:
- A [constant pattern](patterns.md#constant-pattern): to handle the defined values of the `Direction` enumeration.
-- A [discard pattern](patterns.md#discard-pattern): to handle any integer value that doesn't have the corresponding member of the `Direction` enumeration (for example, `(Direction)10`). That pattern makes the `switch` expression [exhaustive](#non-exhaustive-switch-expressions).
+- A [discard pattern](patterns.md#discard-pattern): to handle any integer value that doesn't have the corresponding member of the `Direction` enumeration (for example, `(Direction)10`). That pattern makes the `switch` expression [exhaustive](#nonexhaustive-switch-expressions).
> [!IMPORTANT]
> For information about the patterns supported by the `switch` expression and more examples, see [Patterns](patterns.md).
@@ -43,10 +43,12 @@ A pattern might not be expressive enough to specify the condition for the evalua
The preceding example uses [property patterns](patterns.md#property-pattern) with nested [var patterns](patterns.md#var-pattern).
-## Non-exhaustive switch expressions
+## Nonexhaustive switch expressions
If none of a `switch` expression's patterns matches an input value, the runtime throws an exception. In .NET Core 3.0 and later versions, the exception is a . In .NET Framework, the exception is an . In most cases, the compiler generates a warning if a `switch` expression doesn't handle all possible input values. [List patterns](patterns.md#list-patterns) don't generate a warning when all possible inputs aren't handled.
+For [union types](../builtin-types/union.md), a `switch` expression is exhaustive when it handles all case types. A catch-all arm isn't needed. If the null state of the union's `Value` property is "maybe null," you must also handle `null` to avoid a warning. For more information, see [Union exhaustiveness](../builtin-types/union.md#union-exhaustiveness).
+
> [!TIP]
> To guarantee that a `switch` expression handles all possible input values, provide a `switch` expression arm with a [discard pattern](patterns.md#discard-pattern).
diff --git a/docs/csharp/language-reference/toc.yml b/docs/csharp/language-reference/toc.yml
index 8eeda1ce75b27..d73894371628a 100644
--- a/docs/csharp/language-reference/toc.yml
+++ b/docs/csharp/language-reference/toc.yml
@@ -37,6 +37,9 @@ items:
- name: Struct types
href: ./builtin-types/struct.md
displayName: "struct type, readonly struct, ref struct, readonly ref struct"
+ - name: Union types
+ href: ./builtin-types/union.md
+ displayName: "union type, union declaration, case types"
- name: Ref struct types
href: ./builtin-types/ref-struct.md
displayName: "ref struct, readonly ref struct"
diff --git a/docs/csharp/specification/toc.yml b/docs/csharp/specification/toc.yml
index 2286c56360af5..14f9da84c8270 100644
--- a/docs/csharp/specification/toc.yml
+++ b/docs/csharp/specification/toc.yml
@@ -233,6 +233,10 @@ items:
href: ../../../_csharplang/proposals/csharp-11.0/low-level-struct-improvements.md
- name: Inline arrays
href: ../../../_csharplang/proposals/csharp-12.0/inline-arrays.md
+ - name: Unions
+ items:
+ - name: Union types
+ href: ../../../_csharplang/proposals/unions.md
- name: Interfaces
items:
- name: Variance safety for static interface members
diff --git a/docs/csharp/tour-of-csharp/tips-for-javascript-developers.md b/docs/csharp/tour-of-csharp/tips-for-javascript-developers.md
index 929856c37e212..2abc303450c51 100644
--- a/docs/csharp/tour-of-csharp/tips-for-javascript-developers.md
+++ b/docs/csharp/tour-of-csharp/tips-for-javascript-developers.md
@@ -53,7 +53,7 @@ async Task FetchDataAsync() {
}
```
-Learn more: [Asynchronous programming](../asynchronous-programming/index.md)
+To learn more, see [Asynchronous programming](../asynchronous-programming/index.md).
**Classes:**
@@ -69,7 +69,7 @@ class Point {
record Point(int X, int Y);
```
-Learn more: [Records](../fundamentals/types/records.md)
+To learn more, see [Records](../fundamentals/types/records.md).
**Pattern matching:**
@@ -83,7 +83,7 @@ if (typeof value === "string") { /* ... */ }
if (value is string s) { /* use s */ }
```
-Learn more: [Pattern matching](../fundamentals/functional/pattern-matching.md)
+To learn more, see [Pattern matching](../fundamentals/functional/pattern-matching.md).
## What's new for you in C\#
@@ -108,11 +108,11 @@ Some familiar features and idioms from JavaScript and TypeScript aren't availabl
In addition, a few more TypeScript features aren't available in C#:
-1. ***Union types***: C# doesn't support union types. However, design proposals are in progress.
+1. ***Union types***: Beginning with C# 15, C# supports [union types](../language-reference/builtin-types/union.md). A union defines a closed set of named cases that a value can represent, and the compiler ensures exhaustive pattern matching over those cases.
1. ***Decorators***: C# doesn't have decorators. Some common decorators, such as `@sealed` are reserved keywords in C#. Other common decorators might have corresponding [Attributes](../language-reference/attributes/general.md). For other decorators, you can create your own attributes.
1. ***More forgiving syntax***: The C# compiler parses code more strictly than JavaScript requires.
-If you're building a web application, consider using [Blazor](/aspnet/core/blazor/index) to build your application. Blazor is a full-stack web framework built for C#. Blazor components can run on the server, as .NET assemblies, or on the client using WebAssembly. Blazor supports interop with your favorite JavaScript or TypeScript libraries.
+If you're building a web application, consider using [Blazor](/aspnet/core/blazor/index) to build your application. Blazor is a full-stack web framework built for C#. Blazor components can run on the server, as .NET assemblies, or on the client by using WebAssembly. Blazor supports interop with your favorite JavaScript or TypeScript libraries.
## Next steps
diff --git a/docs/csharp/whats-new/csharp-15.md b/docs/csharp/whats-new/csharp-15.md
index 3b0db85177798..ce0d6df36a172 100644
--- a/docs/csharp/whats-new/csharp-15.md
+++ b/docs/csharp/whats-new/csharp-15.md
@@ -1,22 +1,23 @@
---
title: What's new in C# 15
description: Get an overview of the new features in C# 15. C# 15 ships with .NET 11.
-ms.date: 02/04/2026
+ms.date: 03/20/2026
ms.topic: whats-new
ms.update-cycle: 365-days
ai-usage: ai-assisted
---
# What's new in C# 15
-C# 15 includes the following new features. You can try these features using the latest [Visual Studio 2026](https://visualstudio.microsoft.com/) version or the [.NET 11 preview SDK](https://dotnet.microsoft.com/download/dotnet):
+C# 15 includes the following new features. Try these features by using the latest [Visual Studio 2026](https://visualstudio.microsoft.com/) insiders version or the [.NET 11 preview SDK](https://dotnet.microsoft.com/download/dotnet):
- [Collection expression arguments](#collection-expression-arguments)
+- [Union types](#union-types)
-C# 15 is the latest C# release. C# 15 is supported on **.NET 11**. For more information, see [C# language versioning](../language-reference/configure-language-version.md).
+C# 15 is the latest C# preview release. .NET 11 previews support C# 15. For more information, see [C# language versioning](../language-reference/configure-language-version.md).
You can download the latest .NET 11 preview SDK from the [.NET downloads page](https://dotnet.microsoft.com/download). You can also download [Visual Studio 2026 insiders](https://visualstudio.microsoft.com/vs/), which includes the .NET 11 preview SDK.
-New features are added to the "What's new in C#" page when they're available in public preview releases. The [working set](https://github.com/dotnet/roslyn/blob/main/docs/Language%20Feature%20Status.md#working-set) section of the [roslyn feature status page](https://github.com/dotnet/roslyn/blob/main/docs/Language%20Feature%20Status.md) tracks when upcoming features are merged into the main branch.
+The "What's new in C#" page adds new features when they're available in public preview releases. The [working set](https://github.com/dotnet/roslyn/blob/main/docs/Language%20Feature%20Status.md#working-set) section of the [roslyn feature status page](https://github.com/dotnet/roslyn/blob/main/docs/Language%20Feature%20Status.md) tracks when upcoming features are merged into the main branch.
You can find any breaking changes introduced in C# 15 in our article on [breaking changes](~/_roslyn/docs/compilers/CSharp/Compiler%20Breaking%20Changes%20-%20DotNet%2011.md).
@@ -39,7 +40,36 @@ HashSet set = [with(StringComparer.OrdinalIgnoreCase), "Hello", "HELLO",
// set contains only one element because all strings are equal with OrdinalIgnoreCase
```
-You can learn more about collection expression arguments in the [language reference article on collection expressions](../language-reference/operators/collection-expressions.md#collection-expression-arguments) or the [feature specification](~/_csharplang/proposals/collection-expression-arguments.md). For information on using collection expression arguments in collection initializers, see [Object and Collection Initializers](../programming-guide/classes-and-structs/object-and-collection-initializers.md#collection-expression-arguments).
+To learn more about collection expression arguments, see the [language reference article on collection expressions](../language-reference/operators/collection-expressions.md#collection-expression-arguments) or the [feature specification](~/_csharplang/proposals/collection-expression-arguments.md). For information on using collection expression arguments in collection initializers, see [Object and Collection Initializers](../programming-guide/classes-and-structs/object-and-collection-initializers.md#collection-expression-arguments).
+
+## Union types
+
+C# 15 introduces *union types*, which represent a value that can be one of several *case types*. Declare a union with the `union` keyword:
+
+```csharp
+public record class Cat(string Name);
+public record class Dog(string Name);
+public record class Bird(string Name);
+
+public union Pet(Cat, Dog, Bird);
+```
+
+Unions provide implicit conversions from each case type, and the compiler ensures `switch` expressions are exhaustive across all case types:
+
+```csharp
+Pet pet = new Dog("Rex");
+
+string name = pet switch
+{
+ Dog d => d.Name,
+ Cat c => c.Name,
+ Bird b => b.Name,
+};
+```
+
+Union types first appear in .NET 11 preview 2. In early .NET 11 previews, the `UnionAttribute` and `IUnion` interface aren't included in the runtime, so you must declare them in your project. A later .NET 11 preview will include these runtime types. Also, some features from the [proposal specification](~/_csharplang/proposals/unions.md) aren't yet implemented, including *union member providers*. Those features will be coming in future previews.
+
+For more information, see [Union types](../language-reference/builtin-types/union.md) in the language reference or the [feature specification](~/_csharplang/proposals/unions.md).