union キーワードが使えるようになったResult<T> や Option<T> のような定番パターンを、言語レベルで表現しやすくなるswitch 式で中身の型を安全に分岐でき、未対応のケースがあるとコンパイラが警告してくれる[Union] 属性と IUnion インターフェースが鍵Union type は、ざっくり言うと「この変数には A 型か B 型か C 型のどれかが入る」という型です。
たとえば現実世界でも、同じ「OS情報」という名前であっても、中身は Windows だったり Linux だったり macOS だったりしますよね。
でも、それぞれ持っている情報はバラバラです。
Version だけDistro と VersionName と Versionこういう「似ていない型をひとまとめにしたい」場面では、これまで C# では少し工夫が必要でした。
object に入れるただ、どれも少し面倒です。
object にすると型安全性が消えますし、enum 管理は地味に事故りやすい。個人的には、こういう「本来は言語が面倒を見てほしい部分」をようやく C# が拾ってくれたのはかなりうれしいと思います。
union キーワード記事では、次のような例が紹介されています。
public union SupportedOS (Windows, Linux, MacOS);
これは「SupportedOS という型は、Windows / Linux / MacOS のどれかを持てます」という宣言です。
さらに、こうやって生成した Union は普通に作れます。
SupportedOS os = new SupportedOS(new MacOS("Tahoe", 25));
しかも、型によっては暗黙変換も使えるので、次のように書けます。

SupportedOS os = new MacOS("Tahoe", 25);
見た目がかなり自然です。
「Union を使っている」と意識しなくても、普通の C# の型のように扱えるのがいいところだと思います。
Union type には Value というプロパティがあり、中に入っている値を object? として取り出せます。
Console.WriteLine(os.Value);
ただし、これは「とりあえず中身を見る」ための手段で、実際にはあまりおすすめの使い方ではなさそうです。
本命は switch 式です。
string GetDescription(SupportedOS os) => os switch
{
Windows windows => $"Windows {windows.Version}",
Linux linux => $"{linux.Distro} {linux.Version}",
MacOS macOS => $"MacOS {macOS.Name} ({macOS.Version})",
};
これがかなり気持ちいいです。
Windows なら Windows 用の処理、Linux なら Linux 用の処理、というふうに自然に分岐できます。
しかも、全部のケースを書き忘れるとコンパイラが警告してくれます。
これは地味だけど超重要です。
「あとでバグになる見落とし」を、コンパイラが先回りして教えてくれるからです。
Union type がなかった時代、こういう表現をするには少し苦労しました。
共通の親クラスを作ってまとめる方法です。
ただし、元の型を自由に変更できる場合に限ります。ライブラリ側の型だと、こちらで勝手に親クラスを追加できません。
object に入れる最も雑ですが、最も安全性が落ちます。
中身を取り出すたびにキャストが必要で、ミスると実行時エラーになりやすいです。
「今入っているのは Windows なのか Linux なのか」を別の値で管理する方法です。
ただし、管理項目が増えるとコードがどんどん複雑になります。

こうして見ると、Union type はかなり素直な解決策です。
「いま欲しかったのはこれだよね」と言いたくなるタイプの機能だと思います。
Result<T> や Option<T> も表現しやすい記事では、Union type はかなり広く使えると説明されています。
たとえば、よくある Result<TSuccess, TError> は、成功か失敗かのどちらかを持つ構造です。
これを Union で表すと、考え方としてはかなりきれいです。
public union Result<T>(T, Exception);
同様に、値があるかないかを表す Option<T> も書けます。
public record class None;
public union Option<T>(None, T);
このへんは、関数型言語に慣れている人には「やっと来たか」という感じではないでしょうか。
C# はどちらかというと実用派の言語ですが、こういう表現力が増えると、堅牢なコードを書きやすくなるのが大きいです。
記事時点では、.NET 11 preview 2 以降の SDK が必要です。
ただし、より快適に使うなら preview 4 以降がよいとのことでした。
さらに、プロジェクトファイルで preview の言語機能を有効にします。
<LangVersion>preview</LangVersion>
なお、重要なのは「これはランタイム機能というよりコンパイラ機能だ」という点です。
そのため、.NET 11 SDK を使いながら、古いランタイムをターゲットにすることもできます。ここは少しややこしいですが、要するに「新しいコンパイラが Union を理解してくれる」から動く、という話です。

ただし、古いランタイムや preview 2〜3 を使う場合は、補助用の型を自前で入れる必要があります。
UnionAttributeIUnionpreview 4 以降なら標準で入っています。
記事の面白いところは、Union type の実装が意外と単純だと見せてくれる点です。
コンパイラが生成する型は、おおむねこんな感じです。
[Union]
public struct SupportedOS : IUnion
{
public object? Value { get; }
public SupportedOS(Windows value) => this.Value = (object)value;
public SupportedOS(Linux value) => this.Value = (object)value;
public SupportedOS(MacOS value) => this.Value = (object)value;
}
つまり、
struct として生成される[Union] 属性が付くValue プロパティを持つという構成です。
個人的には、ここがかなり興味深いです。
「すごく新しい言語機能」に見えて、実はかなり現実的な仕組みでできている。こういう話は、技術好きとしてはニヤッとしてしまいます。
[Union] がないと何が起きる?記事では、あえて [Union] 属性を外した例も紹介されています。
すると、MacOS を SupportedOS にそのまま代入できなくなり、switch のパターンマッチも通らなくなります。
つまり、コンパイラは [Union] を見て
switch で各ケースを認識するという特別扱いをしているわけです。
これは「ただの便利な型」ではなく、「言語とコンパイラが協力して意味を理解している」機能だと分かります。
この手の仕組みが増えると、コードは短くなるだけでなく、読みやすさも上がるので嬉しいです。
記事では、OneOf や Sasa のような既存ライブラリにも触れています。
こうしたライブラリは以前から Union 的なことをやってきましたが、C# の言語機能として正式に扱われると、switch のサポートなどでより自然に使えるようになります。
これもかなり重要です。
新機能というと「これから何かを作る人向け」に見えがちですが、実際には既存コードや既存ライブラリの価値を押し上げることも多いんですよね。
私はこういう「新機能がエコシステム全体を少しずつ底上げする」感じが好きです。
.NET 11 / C# 15 の Union type は、ずっと欲しかった人が多いであろう、かなり実用的な追加機能です。
特に良いのは、
switch で素直に分岐できるという点です。
派手さはないけれど、開発体験をかなり押し上げるタイプの機能だと思います。
C# は少しずつ「現代的で表現力の高い言語」へ寄ってきていますが、Union type はその流れを象徴する一歩ではないでしょうか。
参考: .NET (OK, C#) finally gets union types🎉: Exploring the .NET 11 preview - Part 2