55

I need some way to get the Name of a Type, when type.IsGenericType = true.

    Type t = typeof(List<String>);
    MessageBox.Show( ..?.. );

What I want, is a message box to pop up with List showing... how can I do that?

James Kovacs
  • 11,549
  • 40
  • 44
Entity
  • 7,972
  • 21
  • 79
  • 122

11 Answers11

77

You can implement an extension method to get the "friendly name" of a type, like this:

public static class TypeNameExtensions
{
    public static string GetFriendlyName(this Type type)
    {
        string friendlyName = type.Name;
        if (type.IsGenericType)
        {
            int iBacktick = friendlyName.IndexOf('`');
            if (iBacktick > 0)
            {
                friendlyName = friendlyName.Remove(iBacktick);
            }
            friendlyName += "<";
            Type[] typeParameters = type.GetGenericArguments();
            for (int i = 0; i < typeParameters.Length; ++i)
            {
                string typeParamName = GetFriendlyName(typeParameters[i]);
                friendlyName += (i == 0 ? typeParamName : "," + typeParamName);
            }
            friendlyName += ">";
        }

        return friendlyName;
    }
}

With this in your project, you can now say:

MessageBox.Show(t.GetFriendlyName());

And it will display "List<String>".

I know the OP didn't ask for the generic type parameters, but I prefer it that way. ;-)

Namespaces, standard aliases for built-in types, and use of StringBuilder left as an exercise for the reader. ;-)

yoyo
  • 8,310
  • 4
  • 56
  • 50
  • 8
    With new C# loveliness, you can now write all the bit after the backtick check in one (albeit longish) line, and this will also deal with nested generics: `friendlyName += $"<{string.Join(",", type.GetGenericArguments().Select(p => type.GetFriendlyName()))}>"` – joshcomley Nov 28 '17 at 05:34
  • 3
    You might want to use recursion for nested generic types: `string typeParamName = typeParameters[i].GetFriendlyName();` – PeterB Mar 20 '18 at 08:13
  • Note that this does not work for arrays of generic types, they have names such as ``MyType`1[]`` and thus the trailing array brackets `[]` will be removed by `friendlyName.Remove(iBacktick);`. You need to use `type.IsArray` to test for this and `type.GetArrayRank()` to determine how many dimensions the array had. – AnorZaken Jan 29 '20 at 18:32
  • @joshcomley you're going to make the stack unhappy if you use `type` inside the `Select` instead of `p`. ;) – AnorZaken Jan 29 '20 at 18:38
44
Type t = ...;

if (t.IsGenericType)
{
    Type g = t.GetGenericTypeDefinition();

    MessageBox.Show(g.Name);                                // displays "List`1"

    MessageBox.Show(g.Name.Remove(g.Name.IndexOf('`')));    // displays "List"
}
LukeH
  • 263,068
  • 57
  • 365
  • 409
  • 6
    In case you need the type T of a generic type like `List` you can use something like this `t.GetGenericArguments()[0].Name`. I needed this a while ago and couldn't find it anywhere. This will return `string` in the case you have a `List` – Felipe Correa Mar 31 '16 at 00:27
  • If you remove everything after the backtick you loose any array brackets that might be present. – AnorZaken Jan 29 '20 at 20:24
27

My take on yoyo's approach. Ensures more friendly names for primitives, handles arrays and is recursive to handle nested generics. Also unit tests.

    private static readonly Dictionary<Type, string> _typeToFriendlyName = new Dictionary<Type, string>
    {
        { typeof(string), "string" },
        { typeof(object), "object" },
        { typeof(bool), "bool" },
        { typeof(byte), "byte" },
        { typeof(char), "char" },
        { typeof(decimal), "decimal" },
        { typeof(double), "double" },
        { typeof(short), "short" },
        { typeof(int), "int" },
        { typeof(long), "long" },
        { typeof(sbyte), "sbyte" },
        { typeof(float), "float" },
        { typeof(ushort), "ushort" },
        { typeof(uint), "uint" },
        { typeof(ulong), "ulong" },
        { typeof(void), "void" }
    };

    public static string GetFriendlyName(this Type type)
    {
        string friendlyName;
        if (_typeToFriendlyName.TryGetValue(type, out friendlyName))
        {
            return friendlyName;
        }

        friendlyName = type.Name;
        if (type.IsGenericType)
        {
            int backtick = friendlyName.IndexOf('`');
            if (backtick > 0)
            {
                friendlyName = friendlyName.Remove(backtick);
            }
            friendlyName += "<";
            Type[] typeParameters = type.GetGenericArguments();
            for (int i = 0; i < typeParameters.Length; i++)
            {
                string typeParamName = typeParameters[i].GetFriendlyName();
                friendlyName += (i == 0 ? typeParamName : ", " + typeParamName);
            }
            friendlyName += ">";
        }

        if (type.IsArray)
        {
            return type.GetElementType().GetFriendlyName() + "[]";
        }

        return friendlyName;
    }

[TestFixture]
public class TypeHelperTest
{
    [Test]
    public void TestGetFriendlyName()
    {
        Assert.AreEqual("string", typeof(string).FriendlyName());
        Assert.AreEqual("int[]", typeof(int[]).FriendlyName());
        Assert.AreEqual("int[][]", typeof(int[][]).FriendlyName());
        Assert.AreEqual("KeyValuePair<int, string>", typeof(KeyValuePair<int, string>).FriendlyName());
        Assert.AreEqual("Tuple<int, string>", typeof(Tuple<int, string>).FriendlyName());
        Assert.AreEqual("Tuple<KeyValuePair<object, long>, string>", typeof(Tuple<KeyValuePair<object, long>, string>).FriendlyName());
        Assert.AreEqual("List<Tuple<int, string>>", typeof(List<Tuple<int, string>>).FriendlyName());
        Assert.AreEqual("Tuple<short[], string>", typeof(Tuple<short[], string>).FriendlyName());
    }
}
Humberto
  • 7,117
  • 4
  • 31
  • 46
Nick
  • 784
  • 8
  • 12
  • Does this compile? I had to tweak a bit. Edited for corrections. – Humberto May 18 '16 at 17:49
  • 4
    You forgot about Nullable. To prettify nullables you should use something like this: `if (type.GetGenericTypeDefinition() == typeof(Nullable<>)) return type.GetGenericArguments().First().GetFriendlyName() + "?";` – Igor S Apr 06 '17 at 08:58
  • This has the same problem as yoyo's answer in that it doesn't check for arrays and strips away any `[]` brackets from the name. `GetFriendlyName(typeof(Foo[]>))` returns `Foo>`. – AnorZaken Jan 29 '20 at 18:42
  • The aliasing doesn't handle array's either; Use `type.IsArray` and `type.GetElementType()` to resolve aliases for arrays. I updated my answer [about type aliases](https://stackoverflow.com/a/59973036/533837) to show how one can do that. – AnorZaken Jan 29 '20 at 20:16
10
public static class TypeNameExtensions
{
    public static string GetFriendlyName(this Type type)
    {
        var friendlyName = type.Name;
        if (!type.IsGenericType) return friendlyName;

        var iBacktick = friendlyName.IndexOf('`');
        if (iBacktick > 0) friendlyName = friendlyName.Remove(iBacktick);

        var genericParameters = type.GetGenericArguments().Select(x => x.GetFriendlyName());
        friendlyName += "<" + string.Join(", ", genericParameters) + ">";

        return friendlyName;
    }
}
Ali
  • 1,982
  • 1
  • 15
  • 16
10

Assuming you just want to see that its List<T> instead of List<string> you'd need to do:

MessageBox.Show(t.GetGenericTypeDefinition().FullName)

See http://msdn.microsoft.com/en-us/library/system.type.getgenerictypedefinition.aspx

Alex Wiese
  • 8,142
  • 6
  • 42
  • 71
Johannes Rudolph
  • 35,298
  • 14
  • 114
  • 172
7

Here is my take on this. I did not put the backtick check since for what I see, it's always there. You can add it if you want but I like to keep things simple.

public static string GetFriendlyName(this Type type)
{
    if (type.IsGenericType)
    {
        var name = type.Name.Substring(0, type.Name.IndexOf('`'));
        var types = string.Join(",", type.GetGenericArguments().Select(GetFriendlyName));
        return $"{name}<{types}>";
    }
    else
    {
        return type.Name;
    }
}
Yepeekai
  • 2,545
  • 29
  • 22
6

I know this is an old question, but a colleague and myself needed to do this for some intellisense/roslyn work. The optimal solution appeared to be Ali's solution, but it doesn't work for nested types:

    int i = 1; //would work
    List<string> listTest = new List<string>(); //would work
    Dictionary<string, int> dictTest = new Dictionary<string, int>(); //would work
    Dictionary<int, List<string>> nestTest = new Dictionary<int, List<string>>(); //would fail
    Dictionary<int, List<Dictionary<string, List<object>>>> superNestTest = new Dictionary<int, List<Dictionary<string, List<object>>>>(); //would fail
    Dictionary<int, List<Dictionary<string, int>>> superNestTest2 = new Dictionary<int, List<Dictionary<string, int>>>(); //would fail

In order to solve these issues, I converted the function into a recursive method:

public static class TypeExtensions
{
    public static string GetFriendlyName(this Type type)
    {
        string friendlyName = type.FullName;
        if (type.IsGenericType)
        {
            friendlyName = GetTypeString(type);
        }
        return friendlyName;
    }

    private static string GetTypeString(Type type)
    {
        var t = type.AssemblyQualifiedName;

        var output = new StringBuilder();
        List<string> typeStrings = new List<string>();  

        int iAssyBackTick = t.IndexOf('`') + 1;
        output.Append(t.Substring(0, iAssyBackTick - 1).Replace("[", string.Empty));
        var genericTypes = type.GetGenericArguments();

        foreach (var genType in genericTypes)
        {
            typeStrings.Add(genType.IsGenericType ? GetTypeString(genType) : genType.ToString());
        }

        output.Append($"<{string.Join(",", typeStrings)}>");
        return output.ToString();
    }
}

running for the previous examples/test cases yielded the following outputs:

System.Int32
System.Collections.Generic.List<System.String>
System.Collections.Generic.Dictionary<System.String,System.Int32>
System.Collections.Generic.Dictionary<System.Int32,System.Collections.Generic.List<System.String>>
System.Collections.Generic.Dictionary<System.Int32,System.Collections.Generic.List<System.Collections.Generic.Dictionary<System.String,System.Collections.Generic.List<System.Object>>>>
System.Collections.Generic.Dictionary<System.Int32,System.Collections.Generic.List<System.Collections.Generic.Dictionary<System.String,System.Int32>>>

I spent some time trying to resolve the nested types issue so wanted to document this here to ensure anyone else in future can save some considerable time (and headaches!). I have checked the performance as well, and it is in the microseconds to complete (8 microseconds in the case of the last scenario:

Performance results
(Variables names used from original scenario list)
"i" | 43uS
"listTest" | 3uS
"dictTest" | 2uS
"nestTest" | 5uS
"superNestTest" | 9uS
"superNestTest2" | 9uS
Average times after performing the above code 200 times on each scenario

Chris Watts
  • 822
  • 1
  • 9
  • 27
  • 1
    This is the best IMO, should be a standard method in .NET Standard/Core. I changed out the type.FullName and type.AssemblyQualifiedName and type.ToString() for a simple type.Name though because I wanted the shorter names for a code completion engine I'm writing where Roslyn isn't available. – John Ernest Dec 30 '20 at 21:52
5

Here is a complete implementation based on the previous answers supporting both Aliases (including Nullable) and Arrays:

public static class TypeNameExtensions
{
    public static string GetFriendlyName(this Type type, bool aliasNullable = true, bool includeSpaceAfterComma = true)
    {
        TryGetInnerElementType(ref type, out string arrayBrackets);
        if (!TryGetNameAliasNonArray(type, out string friendlyName))
        {
            if (!type.IsGenericType)
            {
                friendlyName = type.Name;
            }
            else
            {
                if (aliasNullable && type.GetGenericTypeDefinition() == typeof(System.Nullable<>))
                {
                    string generics = GetFriendlyName(type.GetGenericArguments()[0]);
                    friendlyName = generics + "?";
                }
                else
                {
                    string generics = GetFriendlyGenericArguments(type, includeSpaceAfterComma);
                    int iBacktick = type.Name.IndexOf('`');
                    friendlyName = (iBacktick > 0 ? type.Name.Remove(iBacktick) : type.Name)
                        + $"<{generics}>";
                }
            }
        }
        return friendlyName + arrayBrackets;
    }

    public static bool TryGetNameAlias(this Type type, out string alias)
    {
        TryGetInnerElementType(ref type, out string arrayBrackets);
        if (!TryGetNameAliasNonArray(type, out alias))
            return false;
        alias += arrayBrackets;
        return true;
    }

    private static string GetFriendlyGenericArguments(Type type, bool includeSpaceAfterComma)
        => string.Join(
            includeSpaceAfterComma ? ", " : ",",
            type.GetGenericArguments().Select(t => t.GetFriendlyName())
            );

    private static bool TryGetNameAliasNonArray(Type type, out string alias)
        => (alias = TypeAliases[(int)Type.GetTypeCode(type)]) != null
        && !type.IsEnum;

    private static bool TryGetInnerElementType(ref Type type, out string arrayBrackets)
    {
        arrayBrackets = null;
        if (!type.IsArray)
            return false;
        do
        {
            arrayBrackets += "[" + new string(',', type.GetArrayRank() - 1) + "]";
            type = type.GetElementType();
        }
        while (type.IsArray);
        return true;
    }

    private static readonly string[] TypeAliases = {
        "void",     // 0
        null,       // 1 (any other type)
        "DBNull",   // 2
        "bool",     // 3
        "char",     // 4
        "sbyte",    // 5
        "byte",     // 6
        "short",    // 7
        "ushort",   // 8
        "int",      // 9
        "uint",     // 10
        "long",     // 11
        "ulong",    // 12
        "float",    // 13
        "double",   // 14
        "decimal",  // 15
        null,       // 16 (DateTime)
        null,       // 17 (-undefined-)
        "string",   // 18
    };
}

Tested with nonsense such as:

var type = typeof(Dictionary<string[,], List<int?[,][]>[,,]>[]);
var name = type.GetFriendlyName();
Console.WriteLine(name);

And it does indeed return: "Dictionary<string[,], List<int?[,][]>[,,]>[]"


Edit: Updated to properly handle enum types.

AnorZaken
  • 1,916
  • 1
  • 22
  • 34
  • (For anyone curious about the 17:th "undefined" special type, it was originally reserved for one more time related type. It only existed as part of an early internal pre-1.0 C# draft, and never made it into any public release as far as I know.) – AnorZaken Dec 30 '20 at 23:08
3

i have improved yoyos version for the usage in Code Generation. Note that all types are now referenced full qualified => global::System.String.

            public static string GetFriendlyTypeName(Type type)
            {
                string friendlyName = type.Name;
                if (type.IsGenericType)
                {
                    int iBacktick = friendlyName.IndexOf('`');
                    if (iBacktick > 0)
                    {
                        friendlyName = friendlyName.Remove(iBacktick);
                    }
                    friendlyName += "<";
                    Type[] typeParameters = type.GetGenericArguments();
                    for (int i = 0; i < typeParameters.Length; ++i)
                    {
                        string typeParamName = GetFriendlyTypeName(typeParameters[i]);
                        friendlyName += (i == 0 ? typeParamName : "," + typeParamName);
                    }
                    friendlyName += ">";
                    friendlyName = "global::" + type.Namespace + "." + friendlyName;
                }
                else
                {
                    friendlyName = "global::" + type.FullName;
                }

                return friendlyName.Replace('+', '.');
            }
Thomas Haller
  • 199
  • 1
  • 11
0

In the latest version of C#, you can use:

var s = x.GetType().ShortDisplayName();

And it returns Thing<IFoo>

Edit: Sorry, that only works as an extension on EF Core. :(

Bill Noel
  • 1,120
  • 9
  • 21
0
string GetFriendlyName(Type type)
    => type.IsArray ? $"{GetFriendlyName(type.GetElementType())}[]"
     : type.IsGenericType ? $"{type.Name.Remove(type.Name.IndexOf('`'))}<{string.Join(", ", type.GetGenericArguments().Select(GetFriendlyName))}>"
     : type.Name;

Works for nested generic types and arrays of Nullable<T>

//outputs Dictionary<String, List<Nullable<Int32>[]>>
GetFriendlyName(typeof(Dictionary<string, List<int?[]>>));

Please, note that most if not all existing answers fails to display array of nullables (int?[])

Liero
  • 25,216
  • 29
  • 151
  • 297