While jason's answer is a good start- it does not cover nullables. I needed a slightly more generic solution that both covered nullables, and given two types, finds the best common type, including implicit and explicit conversions.
(If you need just implicit conversions, the below can probably be modified for such).
/// <summary>Finds the best common type among the given types.</summary>
/// <param name="type1">The first type to check.</param>
/// <param name="type2">The second type to check.</param>
/// <returns>The best common type.</returns>
public static Type FindBestCommonType(Type type1, Type type2)
{
if (type1 == null && type2 == null) throw new ArgumentNullException("One of the two types must be non-null.");
if (type1 == null) return ensureNullable(type2);
if (type2 == null) return ensureNullable(type1);
if (type1 == type2) return type1;
if (type1.IsAssignableFrom(type2)) return type1;
if (type2.IsAssignableFrom(type1)) return type2;
Type bestCommonType = null;
var type1Underlying = Nullable.GetUnderlyingType(type1);
var type2Underlying = Nullable.GetUnderlyingType(type2);
var type1Nullable = type1Underlying != null;
var type2Nullable = type2Underlying != null;
var resultMustBeNullable = type1Nullable || type2Nullable;
type1 = type1Underlying ?? type1;
type2 = type2Underlying ?? type2;
//If our nullable-stripped types are equivalent, send back the nullable version
if (type1 == type2)
return typeof(Nullable<>).MakeGenericType(type1);
var type1Convertibles = _convertibleTypes.GetValueOrDefault(type1);
var type2Convertibles = _convertibleTypes.GetValueOrDefault(type2);
bestCommonType = type1Convertibles?.Contains(type2) == true ? type1
: type2Convertibles?.Contains(type1) == true ? type2
: null;
//Check for implicit or explicit conversion
if (bestCommonType == null)
{
if (type1.GetMethods(BindingFlags.Public | BindingFlags.Static)
.Any(m => m.ReturnType == type2 && (m.Name == "op_Implicit" || m.Name == "op_Explicit")))
bestCommonType = type2;
else if (type2.GetMethods(BindingFlags.Public | BindingFlags.Static)
.Any(m => m.ReturnType == type1 && (m.Name == "op_Implicit" || m.Name == "op_Explicit")))
bestCommonType = type1;
}
if (resultMustBeNullable && bestCommonType != null && bestCommonType != typeof(object))
bestCommonType = typeof(Nullable<>).MakeGenericType(bestCommonType);
return bestCommonType ?? typeof(object);
//Function to ensure that the given type can hold nulls - if its a reference type it does nothing - if it's a value type it ensures it is Nullable<T>
static Type ensureNullable(Type t) => t.IsValueType && Nullable.GetUnderlyingType(t) == null ? typeof(Nullable<>).MakeGenericType(t) : t;
}
private static readonly Dictionary<Type, HashSet<Type>> _convertibleTypes = new Dictionary<Type, HashSet<Type>>() {
{ typeof(decimal), new HashSet<Type> { typeof(sbyte), typeof(byte), typeof(short), typeof(ushort), typeof(int), typeof(uint), typeof(long), typeof(ulong), typeof(char) } },
{ typeof(double), new HashSet<Type> { typeof(sbyte), typeof(byte), typeof(short), typeof(ushort), typeof(int), typeof(uint), typeof(long), typeof(ulong), typeof(char), typeof(float) } },
{ typeof(float), new HashSet<Type> { typeof(sbyte), typeof(byte), typeof(short), typeof(ushort), typeof(int), typeof(uint), typeof(long), typeof(ulong), typeof(char) } },
{ typeof(ulong), new HashSet<Type> { typeof(byte), typeof(ushort), typeof(uint), typeof(char) } },
{ typeof(long), new HashSet<Type> { typeof(sbyte), typeof(byte), typeof(short), typeof(ushort), typeof(int), typeof(uint), typeof(char) } },
{ typeof(uint), new HashSet<Type> { typeof(byte), typeof(ushort), typeof(char) } },
{ typeof(int), new HashSet<Type> { typeof(sbyte), typeof(byte), typeof(short), typeof(ushort), typeof(char) } },
{ typeof(ushort), new HashSet<Type> { typeof(byte), typeof(char) } },
{ typeof(short), new HashSet<Type> { typeof(byte) } }
};
}
This can be overloaded to find the best common type for an arbitrarily large set of types:
/// <summary>Finds the best common type among the given types.</summary>
/// <param name="types">The types to check.</param>
/// <returns>The best common type.</returns>
public static Type FindBestCommonType(params Type[] types)
{
if (types == null) throw new ArgumentNullException(nameof(types));
var filteredTypes = types.Distinct().ToList();
if (filteredTypes.Count == 0) throw new InvalidOperationException("No types were provided");
var bestCommonType = filteredTypes[0];
foreach (var type in filteredTypes.Skip(1))
{
bestCommonType = FindBestCommonType(type, bestCommonType);
}
return bestCommonType;
}
We can then use this to widen unknown types at runtime:
/// <summary>
/// Attempts to widen the given objects so that they are both compatible types.
/// </summary>
/// <param name="o1">The first object, passed by reference.</param>
/// <param name="o2">The second object, passed by reference.</param>
public static void WidenToEqualTypes(ref object o1, ref object o2)
{
var type1 = o1.GetType();
var type2 = o2.GetType();
var bestCommonType = FindBestCommonType(type1, type2);
o1 = Convert.ChangeType(o1, bestCommonType);
o2 = Convert.ChangeType(o2, bestCommonType);
}
Unit tests:
[TestCase(typeof(long), new[] { typeof(int), typeof(long) })]
[TestCase(typeof(long?), new[] {typeof(int), typeof(long?)})]
[TestCase(typeof(long?), new[] {typeof(int?), typeof(long)})]
[TestCase(typeof(double?), new[] {typeof(int?), typeof(double)})]
[TestCase(typeof(decimal), new[] {typeof(long), typeof(decimal)})]
[TestCase(typeof(double), new[] { typeof(float), typeof(double) })]
[TestCase(typeof(bool?), new[] {typeof(bool?), typeof(bool)})]
[TestCase(typeof(bool?), new[] { null, typeof(bool) })]
[TestCase(typeof(string), new[] { typeof(string), null })]
[TestCase(typeof(DateTime), new[] {typeof(DateOnly), typeof(DateTime)})]
[TestCase(typeof(DateTime?), new[] {typeof(DateOnly?), typeof(DateTime)})]
[TestCase(typeof(DateTime?), new[] {typeof(DateTime?), typeof(DateOnly)})]
[TestCase(typeof(object), new[] {typeof(string), typeof(int)})]
[TestCase(typeof(Guid?), new[] {typeof(Guid), typeof(Guid?)})]
[TestCase(typeof(double?), new[] { typeof(int), typeof(long?), typeof(double) })]
[TestCase(typeof(DateTime?), new[] { typeof(DateTime), typeof(DateOnly?), typeof(DateOnly) })]
[TestCase(typeof(IEnumerable<int>), new[] { typeof(IEnumerable<int>), typeof(List<int>) })]
[Description("Finds the best common type that all the supplied types are convertible to.")]
public void BestCommonTypesTests(Type expected, Type[] types)
{
Assert.AreEqual(expected, Util.FindBestCommonType(types));
}