How to tell if Type A is implicitly convertible to Type B

Note that IsAssignableFrom does NOT solve your problem. You have to use Reflection like so. Note the explicit need to handle the primitive types; these lists are per §6.1.2 (Implicit numeric conversions) of the specification.

static class TypeExtensions { 
    static Dictionary<Type, List<Type>> dict = new Dictionary<Type, List<Type>>() {
        { typeof(decimal), new List<Type> { typeof(sbyte), typeof(byte), typeof(short), typeof(ushort), typeof(int), typeof(uint), typeof(long), typeof(ulong), typeof(char) } },
        { typeof(double), new List<Type> { typeof(sbyte), typeof(byte), typeof(short), typeof(ushort), typeof(int), typeof(uint), typeof(long), typeof(ulong), typeof(char), typeof(float) } },
        { typeof(float), new List<Type> { typeof(sbyte), typeof(byte), typeof(short), typeof(ushort), typeof(int), typeof(uint), typeof(long), typeof(ulong), typeof(char), typeof(float) } },
        { typeof(ulong), new List<Type> { typeof(byte), typeof(ushort), typeof(uint), typeof(char) } },
        { typeof(long), new List<Type> { typeof(sbyte), typeof(byte), typeof(short), typeof(ushort), typeof(int), typeof(uint), typeof(char) } },
        { typeof(uint), new List<Type> { typeof(byte), typeof(ushort), typeof(char) } },
        { typeof(int), new List<Type> { typeof(sbyte), typeof(byte), typeof(short), typeof(ushort), typeof(char) } },
        { typeof(ushort), new List<Type> { typeof(byte), typeof(char) } },
        { typeof(short), new List<Type> { typeof(byte) } }
    };
    public static bool IsCastableTo(this Type from, Type to) { 
        if (to.IsAssignableFrom(from)) { 
            return true; 
        }
        if (dict.ContainsKey(to) && dict[to].Contains(from)) {
            return true;
        }
        bool castable = from.GetMethods(BindingFlags.Public | BindingFlags.Static) 
                        .Any( 
                            m => m.ReturnType == to &&  
                            (m.Name == "op_Implicit" ||  
                            m.Name == "op_Explicit")
                        ); 
        return castable; 
    } 
} 

Usage:

bool b = typeof(A).IsCastableTo(typeof(B));

Implicit conversions you'll need to consider:

  • Identity
  • sbyte to short, int, long, float, double, or decimal
  • byte to short, ushort, int, uint, long, ulong, float, double, or decimal
  • short to int, long, float, double, or decimal
  • ushort to int, uint, long, ulong, float, double, or decimal
  • int to long, float, double, or decimal
  • uint to long, ulong, float, double, or decimal
  • long to float, double, or decimal
  • ulong to float, double, or decimal
  • char to ushort, int, uint, long, ulong, float, double, or decimal
  • float to double
  • Nullable type conversion
  • Reference type to object
  • Derived class to base class
  • Class to implemented interface
  • Interface to base interface
  • Array to array when arrays have the same number of dimensions, there is an implicit conversion from the source element type to the destination element type and the source element type and the destination element type are reference types
  • Array type to System.Array
  • Array type to IList<> and its base interfaces
  • Delegate type to System.Delegate
  • Boxing conversion
  • Enum type to System.Enum
  • User defined conversion (op_implicit)

I assume you're looking for the latter. You'll need to write something resembling a compiler to cover all of them. Notable is that System.Linq.Expressions.Expression didn't attempt this feat.


The accepted answer to this question handles many cases, but not all. For example, here are just a few valid casts/conversions which are not handled correctly:

// explicit
var a = (byte)2;
var b = (decimal?)2M;

// implicit
double? c = (byte)2;
decimal? d = 4L;

Below, I've posted an alternate version of this function which specifically answers the question of IMPLICIT casts and conversions. For more details, the test suite I used to verify it, and the EXPLICIT cast version, please check out my post on the subject.

public static bool IsImplicitlyCastableTo(this Type from, Type to)
{
    if (from == null) { throw new ArgumentNullException(nameof(from)); }
    if (to == null) { throw new ArgumentNullException(nameof(to)); }

    // not strictly necessary, but speeds things up
    if (to.IsAssignableFrom(from))
    {
        return true;
    }

    try
    {
        // overload of GetMethod() from http://www.codeducky.org/10-utilities-c-developers-should-know-part-two/ 
        // that takes Expression<Action>
        ReflectionHelpers.GetMethod(() => AttemptImplicitCast<object, object>())
            .GetGenericMethodDefinition()
            .MakeGenericMethod(from, to)
            .Invoke(null, new object[0]);
        return true;
    }
    catch (TargetInvocationException ex)
    {
        return = !(
            ex.InnerException is RuntimeBinderException
            // if the code runs in an environment where this message is localized, we could attempt a known failure first and base the regex on it's message
            && Regex.IsMatch(ex.InnerException.Message, @"^The best overloaded method match for 'System.Collections.Generic.List<.*>.Add(.*)' has some invalid arguments$")
        );
    }
}

private static void AttemptImplicitCast<TFrom, TTo>()
{
    // based on the IL produced by:
    // dynamic list = new List<TTo>();
    // list.Add(default(TFrom));
    // We can't use the above code because it will mimic a cast in a generic method
    // which doesn't have the same semantics as a cast in a non-generic method

    var list = new List<TTo>(capacity: 1);
    var binder = Microsoft.CSharp.RuntimeBinder.Binder.InvokeMember(
        flags: CSharpBinderFlags.ResultDiscarded, 
        name: "Add", 
        typeArguments: null, 
        context: typeof(TypeHelpers), // the current type
        argumentInfo: new[] 
        { 
            CSharpArgumentInfo.Create(flags: CSharpArgumentInfoFlags.None, name: null), 
            CSharpArgumentInfo.Create(
                flags: CSharpArgumentInfoFlags.UseCompileTimeType, 
                name: null
            ),
        }
    );
    var callSite = CallSite<Action<CallSite, object, TFrom>>.Create(binder);
    callSite.Target.Invoke(callSite, list, default(TFrom));
}