Search code examples
c#oopunion-typesdiscriminated-union

What are workaround options for implementing union types in C#


I am trying to implement a system where I need to have a C# class with property which type can be one of types.

In F# this features called "Discriminated Unions", in TypeScript it is called "Union Types".

Here is an example of how I could implement it in TypeScript:

class Something {
    public myField?: boolean | Date;
}

let something = new Something();

something.myField = true;
something.myField = new Date();

As far as I know it's not possible in C#. What are the workarounds (ideally, without 3rd party extensions)?

How would you (as C# developer) implement this?

Options I considered

  1. Using object type might work in my case but I would like to have more precise solution.

  2. I've also tried to use implicit conversion but it looks too hacky


class MyFieldType {
  private bool _valueBool;
  private DateTime _valueDateTime;

   public MyFieldType(DateTime value) {
    _valueDateTime = value;
  }
  
  public MyFieldType(bool value) {
    _valueBool = value;
  }
  
  public bool GetBool() {
    return _valueBool;
  }
  
  public DateTime GetDateTime() {
    return _valueDateTime;
  }

  public static implicit operator MyFieldType(bool value) {
    return new MyFieldType(value);
  }
  
  public static implicit operator MyFieldType(DateTime value) {
    return new MyFieldType(value);
  }
  
  public static implicit operator DateTime(MyFieldType value) {
    return value.GetDateTime();
  }
  
  public static implicit operator bool(MyFieldType value) {
    return value.GetBool();
  }
}

class MyClass {
  public MyFieldType? MyField;
}

Solution

  • Apart from all the hacky options that I've seen over the years, I'm aware of two good, faithful representations: Church encoding and the Visitor design pattern.

    Church encoding

    One way to Church-encode the OP example is this:

    public sealed class ChurchSomething
    {
        private readonly IChurchSomething imp;
    
        private ChurchSomething(IChurchSomething imp)
        {
            this.imp = imp;
        }
    
        public static ChurchSomething CreateBool(bool b)
        {
            return new ChurchSomething(new Boolean(b));
        }
    
        public static ChurchSomething CreateDate(DateTime date)
        {
            return new ChurchSomething(new Date(date));
        }
    
        public T Match<T>(Func<bool, T> onBoolean, Func<DateTime, T> onDate)
        {
            return this.imp.Match(onBoolean, onDate);
        }
    
        private interface IChurchSomething
        {
            T Match<T>(Func<bool, T> onBoolean, Func<DateTime, T> onDate);
        }
    
        private sealed class Boolean(bool b) : IChurchSomething
        {
            public T Match<T>(Func<bool, T> onBoolean, Func<DateTime, T> onDate)
            {
                return onBoolean(b);
            }
        }
    
        private sealed class Date(DateTime dt) : IChurchSomething
        {
            public T Match<T>(Func<bool, T> onBoolean, Func<DateTime, T> onDate)
            {
                return onDate(dt);
            }
        }
    }
    

    While Church-encoding is the simplest way to represent a sum type, many people find that it doesn't look object-oriented. If so, you can refactor it to a Visitor.

    Visitor

    The Visitor-based representation of the OP example may look like this:

    public interface ISomethingVisitor<T>
    {
        T VisitBool(bool b);
        T VisitDate(DateTime dt);
    }
    
    public sealed class VisitorSomething
    {
        private readonly IVisitorSomething imp;
    
        private VisitorSomething(IVisitorSomething imp)
        {
            this.imp = imp;
        }
    
        public static VisitorSomething CreateBool(bool b)
        {
            return new VisitorSomething(new Boolean(b));
        }
    
        public static VisitorSomething CreateDate(DateTime date)
        {
            return new VisitorSomething(new Date(date));
        }
    
        public T Accept<T>(ISomethingVisitor<T> visitor)
        {
            return this.imp.Accept(visitor);
        }
    
        private interface IVisitorSomething
        {
            T Accept<T>(ISomethingVisitor<T> visitor);
        }
    
        private sealed class Boolean(bool b) : IVisitorSomething
        {
            public T Accept<T>(ISomethingVisitor<T> visitor)
            {
                return visitor.VisitBool(b);
            }
        }
    
        private sealed class Date(DateTime dt) : IVisitorSomething
        {        public T Accept<T>(ISomethingVisitor<T> visitor)
            {
                return visitor.VisitDate(dt);
            }
        }
    }
    

    In both of these examples, I've defined some of the interfaces as nested private to the implementation itself. I often do this, because in these cases, the purposes of the interfaces is just to make the implementation take advantage of polymorphism. Making those interfaces public is also possible, but I'm sometimes concerned that naive client developers will understand that as signalling intent that they have to implement those interfaces - which is not the case.