Search code examples
c#typestype-constraints

Constrain return type to be subtype of this


I have the following interface:

internal interface ITyped<EnumType> where EnumType : struct, Enum
{
    public EnumType Type { get; }

    // !!! this method must always return a subtype* of the implementing class
    internal static Type TypeFromEnum(EnumType value) => throw new NotImplementedException();
}

I need the method TypeFromEnum to always return a type that is a subtype* of the implementing class. Is there a way to write that condition down in code, maybe with some kind of where constraint ?

(*) By "subtype" I mean either the class type itself or a real child type, so something that could be captured by where Sub : Base if Sub and Base were type parameters.


Use case: I have a bunch of classes with a one-level inheritance hierarchy

public class A { ... }
public class A1 : A { ... }
public class A2 : A { ... }
public class A3 : A { ... }

public abstract class B { ... }
public class B1 { ... }
public class B2 { ... }

and I need to (de-)serialize them from/to json using System.Text.Json. In order to make that possible, A and B have a Type property of some enum type. For deserialization I have a custom converter that reads the content of that type entry as an enum, and then calls TypeFromEnum to determine which type it should deserialize to. Here are the enums and relevant content of the classes:

public enum AType { a, a1, a2, a3 }

public enum BType { b1, b2 }


public class A : ITyped<AEnum>
{
    public AEnum Type { get; }
    internal static Type TypeFromEnum(AEnum value)
    {
        switch (value)
        {
            case AType.a: return typeof(A);
            case AType.a1: return typeof(A1);
            case AType.a2: return typeof(A2);
            case AType.a3: return typeof(A3);
            default: throw new InvalidOperationException(); // should never be called
        }
    }
    ...
}


public abstract class B : ITyped<BEnum>
{
    public BEnum Type { get; }
    internal static Type TypeFromEnum(BEnum value)
    {
        switch (value)
        {
            case BType.b1: return typeof(B1);
            case BType.b2: return typeof(B2);
            default: throw new InvalidOperationException(); // should never be called
        }
    }
    ...
}

Solution

  • If we rely on the switch-case statement in the implementations of TypeFromEnum to provide us the type based on the enum value (as the other answer does), we can still enforce compile time safety from this point forward by changing the signature to return a custom IAssignableTo<out T> instead of Type.

    We need two additional types to achieve that:

    public class TypeOf<T> : IAssignableTo<T> {
        public Type Type => typeof(T);
    }
    
    public interface IAssignableTo<out T> {
        Type Type { get; }
    }
    

    They help us exploit interface covariance which fits your requirement for "subtypes" (will be clear how in the example below)

    We then rewrite the interface to accept another generic parameter ClassType that we constrain to types that implement this interface. This help us change the return type of TypeFromEnum to IAssignableTo<ClassType> from Type. A value of the interface IAssignableTo<ClassType> has a Type property which will get us the type instance via typeof(ClassType) ( see implementation above).

    internal interface ITyped<EnumType, ClassType> 
        where EnumType : struct, Enum
        where ClassType: ITyped<EnumType, ClassType> {
        public EnumType Type { get; }
    
        // !!! this method must always return a subtype* of the implementing class -> it does now
        internal static IAssignableTo<ClassType> TypeFromEnum(EnumType value) => throw new NotImplementedException();
    }
    

    and the implementation:

    public enum AType { a, a1, a2, a3 }
    
    public class B { }
    
    public class A1 : A { }
    
    public class A : ITyped<AType, A> {
        public AType Type { get; }
    
        internal static IAssignableTo<A> TypeFromEnum(AType value) {
            switch (value) {
                case AType.a: return new TypeOf<A>();
    
                // IAssignableTo<A> covariance here allows us to return
                // TypeOf<A1> for a value of IAssignableTo<A>
                case AType.a1: return new TypeOf<A1>();
    
                //CS0266 Cannot implicitly convert type 'TypeOf<B>'
                // to 'IAssignableTo<A>'. 
                // An explicit conversion exists (are you missing a cast?)
                case AType.a2: return new TypeOf<B>(); // compile time error
    
    
                default: throw new InvalidOperationException(); // should never be called
            }
        }
    }
    

    To consume you just need to call the Type property of the returned value:

    IAssignableTo<A> typeOf = A.TypeFromEnum(AType.a1);
    Type aType = typeOf.Type;
    

    in your use case maybe where A is BaseType generic parameter to another class:

    IAssignableTo<BaseType> typeOf = _TypeFromEnum(value.Type);
    Type baseType = typeOf.Type;
    

    this helps us eliminate the runtime check from the other answer:

    public Type SafeTypeFromEnum(EnumTypevalue value)
    {
        var result = TypeFromEnum(value);
        if (!result.IsSubclassOf(typeof(EnumTypevalue)))
            throw new Exception();
    
        return result;
    }