meetings/2026/LDM-2026-02-04.md
Champion issue: https://github.com/dotnet/csharplang/issues/9662
Spec: https://github.com/dotnet/csharplang/blob/d614288f7dab369f763520ba48b12623c34d8542/proposals/unions.md#union-declarations
Working group notes: https://github.com/dotnet/csharplang/blob/d614288f7dab369f763520ba48b12623c34d8542/meetings/working-groups/discriminated-unions/union-patterns-update.md
Today, we went over a few open questions in unions.
When a union has multiple reference type case types, passing null creates ambiguity about which constructor to
use. For example, with Pet(Dog) and Pet(Bird?) constructors, null could apply to either.
Currently, the compiler picks an arbitrary applicable constructor when there is ambiguity, based on the assumption
that well-formed unions have constructors that behave identically for the same input. However, this can lead to
surprising behavior: if the compiler picks the Dog constructor but Bird? is the one intended to accept null,
a nullability warning will be issued even though the union author intended null to be valid.
We discussed several approaches:
We liked options 1 and 2 here. Option 3 is not how the compiler works today, and doesn't solve the scenario in nullable disabled locations. There are also scenarios where null isn't involved, such as a union of two interfaces and a type that implements both. We also brought up operator conversions, as union conversions are very similar to user-defined operators. We like that analogy, and want to apply similar rules where applicable.
The working group will consider requiring explicit disambiguation when ambiguity cannot be resolved through standard betterness rules, similar to how conversion operators are resolved.
The current design uses the IUnion interface to indicate that a type should be treated as a union. This causes
problems:
IUnion without becoming a union (it might just want runtime participation)The proposal is to use a [Union] attribute instead, with the compiler looking for members by shape rather than
through the interface. The IUnion and IUnion<TUnion> interfaces would still exist for runtime scenarios (such
as generic code that queries whether something is a union or accesses its value), but they would be optional and
not required for compiler recognition.
Approved. The design will change from requiring IUnion interface implementation to using a [Union] attribute
for compiler recognition. Members will be looked up by shape (constructors with case type parameters, a Value
property).
Constructors have limitations: they cannot be defined on a type other than the one they construct, and they cannot be inherited or delegated. Static factory methods provide more flexibility for scenarios involving type hierarchies, object pooling, or delegating union member definitions to a separate type.
The proposal is to allow public static Create methods as an alternative to constructors for defining case types.
When factory methods are present, they take precedence over constructors.
There was significant concern about implicit recognition of factory methods based on shape alone. A method like
Parse(string) returning the union type could be unintentionally picked up as defining a string case type. Even
if we restrict to just a single name (Create), we don't have enthusiasm for the magic shape recognition among
different overloads here.
The LDM expressed strong support for requiring explicit markers rather than relying on implicit shape matching.
The proposal of a [UnionCase] attribute was well-received: authors would mark constructors, factory methods, or
even user-defined implicit conversion operators that should contribute to the union's case types.
A follow-up question arose: should the [UnionCase] attribute be required on everything, including constructors,
or should there be a simple default mode where constructors work implicitly? Two positions emerged:
Attributes on everything: Any member used by the compiler for union behavior must be marked with [UnionCase].
Default behavior for constructors: If a union type has only constructors (no factory methods or conversion
operators), they work implicitly without attributes. Once you want any customization (factory methods, conversion
operators, or selective inclusion of constructors), you must use [UnionCase] on all participating members.
A read-of-the-room vote showed preference for the second approach: allow the simple constructor-only case to work without attributes, while requiring explicit marking for any advanced scenarios. However, we want the working group to take a look at the implications and come back with a concrete proposal and recommendations.
Move forward with the [UnionCase] attribute approach. The working group will produce a concrete proposal with
specific rules. There is no requirement for a specific factory method name when the attribute is used.
This proposal addresses scenarios where the union's desired behavior conflicts with its public surface area. Two cases were identified:
The idea was to allow delegating union member lookup to a separate "provider" type (e.g., a nested interface).
Given the decision to use explicit [UnionCase] attributes, the first scenario is largely addressed: members won't
be picked up unless explicitly marked. For the second scenario, user-defined implicit conversion operators (if
supported) could address types like Result<T> that expose conversions but not constructors. However, we don't have
good examples of what could actually need this ability. Given that, we will simply table this addition for now, and
come back when we have concrete scenarios to address.
This feature will be saved for later. The explicit attribute approach addresses the primary motivating scenarios. If compelling use cases emerge that require full delegation to a provider type, the design space remains open.
Throughout the discussion, a general principle emerged: the LDM is comfortable with some implicit behavior for
well-known members (like recognizing a property named Value), but prefers explicit marking for case type
definitions due to the inherent variability and potential for false positives. It's possible further consideration
of real use cases may cause us to want to go even further into the explicit territory (explicitly marking Value or
the TryValue methods, for example), but the working group will explore this space.