Skip to content

Ad-Hoc Union: Allow _valueIndex = 0 as supported state #22

Closed

Description

@thomashilzendegen

I would like to use the default state of the struct-based union as a valid combination. For my particular example, the struct MaybeOf<T> is the logical value Null when it is "just default" (but the usage of default outside of the struct should still not be possible by using IDisallowDefaultValue).

Activity

  1. PawelGerr commented on Aug 5, 2026

    @PawelGerr
    Owner

    Implemented and coming with the release 10.5.0-beta02.

    There is a new setting DefaultValueHandling on the union attributes. With UnionDefaultValueHandling.MapToFirstMember, the default value of a struct-based ad-hoc union represents the first member (T1) instead of being an invalid state. Your MaybeOf<T> looks like this:

    public readonly record struct Null;  // stateless marker: "no value"
    
    [Union<Null, TypeParamRef1>(
       T1IsStateless = true,
       T2Name = "Value",
       DefaultValueHandling = UnionDefaultValueHandling.MapToFirstMember)]
    public readonly partial struct MaybeOf<T>;
    MaybeOf<string> none = default;      // valid: represents the Null state
    Console.WriteLine(none.IsNull);      // True
    
    MaybeOf<string> some = MaybeOf<string>.CreateValue("hello");
    Console.WriteLine(some.IsNull);      // False
    Console.WriteLine(some.AsValue);     // hello

    Details:

    • Internally the discriminator becomes 0-based, so default(MaybeOf<T>), new MaybeOf<T>() and a constructed Null instance are physically identical: equality, GetHashCode, IsNull, Switch, Map and ToString all work without a special case. Nothing throws anymore for the default value.
    • Constraints: the union must be a struct (TTRESG081) and T1 must be stateless via T1IsStateless = true (TTRESG082). Only T1 can be the default state; if another member should represent "no value", reorder the type parameters.
    • Because a type parameter is involved, factory methods are generated for all members: MaybeOf<T>.CreateNull() and MaybeOf<T>.CreateValue(...). A static property like None is not generated; add it yourself if you prefer that style: public static MaybeOf<T> None => default;
    • An implicit conversion from T is not generated for type-parameter members, because a user-defined conversion from a type parameter breaks when T is instantiated with an interface or object. Use the factory methods instead, or declare the operator manually in your partial part: public static implicit operator MaybeOf<T>(T? value) => value is null ? None : CreateValue(value); (For non-generic unions like [Union<Null, int>] the implicit conversion from the member type is generated as usual.)

    Regarding IDisallowDefaultValue: the interface is now public, so the #pragma warning disable TTRESG1000 workaround is no longer necessary. With MapToFirstMember the generator no longer emits the interface, because the default value is valid and banning it makes no sense by default. If you still want to force consumers to use a named accessor instead of writing default, implement the interface manually; the analyzer then reports TTRESG047 on default/new() again, while the runtime value stays a fully functional Null state:

    [Union<Null, TypeParamRef1>(
       T1IsStateless = true,
       T2Name = "Value",
       DefaultValueHandling = UnionDefaultValueHandling.MapToFirstMember)]
    public readonly partial struct MaybeOf<T> : IDisallowDefaultValue
    {
       public static MaybeOf<T> None => default;
    }

    Documentation: Discriminated Unions › Customization

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions