Dictionary<string, T> is translated to System.Collections.Generic.KeyValuePair<string, dtos.Component3>[] instead of { [key: string]: T; }

Issue #99 new
Mattijs Perdeck
created an issue

The C# definition:

namespace dtos
{
    public class Component3
    {
        public string Manufacturer { get; set; }
    }

    public class Vehicle
    {
        public Dictionary<string, Component3> Components { get; set; }
    }
}

is translated to:

declare module dtos {
    interface Vehicle {
        Components: System.Collections.Generic.KeyValuePair<string, dtos.Component3>[];
    }
    interface Component3 {
        Manufacturer: string;
    }
}

declare module System.Collections.Generic {
    interface KeyValuePair<TKey, TValue> {
        Key: TKey;
        Value: TValue;
    }
}

That is, a Dictionary is regarded as a collection of KeyValuePair objects. Which formally it is according to its C# definition. But this translation stops you from accessing values by their keys in a way you'd expect in JavaScript, such as:

var component = components["Engine"];

In my view, it would be much better to translate Dictionary<string, T> into { [key: string]: T; }, so the above example would translate to:

declare module dtos {
    interface Vehicle {
        Components: { [key: string]: dtos.Component3; };
    }
    interface Component3 {
        Manufacturer: string;
    }
}

I think JavaScript allows both strings and numbers as indexers, so keys of a numeric type could get the same treatment. But it would be different for Dictionaries with key type other than string or number.

Comments (5)

  1. Schirkan

    I had the same problem.

    Solution: Add a custom TypeFormatter to format KeyValuePair<>

    public class MyTypeScriptGenerator
    {
        private TsGenerator _tsGenerator = new TsGenerator();
    
        public string GetModelInterfaces(List<Type> types)
        {
            // add TypeFormatter
            var ts = TypeScript.Definitions(_tsGenerator).WithMemberTypeFormatter(MemberTypeFormatter);
    
            // add Types
            foreach (var type in types)
            {
                ts.For(type);
            }
    
            // generate TS
            return ts.Generate();
        }
    
        private string MemberTypeFormatter(TsProperty tsProperty, string memberTypeName)
        {
            var collectionType = tsProperty.PropertyType as TsCollection;
            if (collectionType != null)
            {
                if (IsSubclassOfRawGeneric(typeof(KeyValuePair<,>), collectionType.ItemsType.Type))
                {
                    var keyType = _tsGenerator.GetFullyQualifiedTypeName(tsProperty.GenericArguments[0]);
                    var valueType = _tsGenerator.GetFullyQualifiedTypeName(tsProperty.GenericArguments[0]);
                    return "{ [key: " + keyType + "]: " + valueType + " }";
                }
            }
            return _tsGenerator.DefaultMemberTypeFormatter(tsProperty, memberTypeName);
        }
    
        private static bool IsSubclassOfRawGeneric(Type generic, Type toCheck)
        {
            while (toCheck != null && toCheck != typeof(object))
            {
                var cur = toCheck.IsGenericType ? toCheck.GetGenericTypeDefinition() : toCheck;
                if (generic == cur)
                {
                    return true;
                }
                toCheck = toCheck.BaseType;
            }
            return false;
        }
    }
    

    This will still generate an unnecessary interface:

    declare namespace System.Collections.Generic {
        interface KeyValuePair<TKey, TValue> {
            key: TKey;
            value: TValue;
        }
    }
    

    But your own interfaces will use the correct types.

  2. VilenT

    This is a little bit enhanced verion of your generator

    public static string MemberTypeFormatter(TsProperty tsProperty, string memberTypeName)
            {
                var collectionType = tsProperty.PropertyType as TsCollection;
                if (collectionType == null)
                {
                    return TsGenerator.DefaultMemberTypeFormatter(tsProperty, memberTypeName);
                }
    
                if (ReflectionHelper.IsSubclassOfOpenGeneric(typeof(KeyValuePair<,>), collectionType.ItemsType.Type))
                {
                    var keyGenType = tsProperty.GenericArguments[0];
                    if (keyGenType.Type == typeof(string))
                    {
                        var keyType = TsGenerator.GetFullyQualifiedTypeName(keyGenType);
                        var valueGenType = tsProperty.GenericArguments[1];
                        var typeName = TsGenerator.GetFullyQualifiedTypeName(valueGenType);
    
                        var valueType = typeName;
                        if (valueGenType.IsCollection())
                        {
                            valueType += Enumerable.Repeat("[]", ((TsCollection)valueGenType).Dimension).ToString(string.Empty);
                        }
    
                        return $"{{ [key: {keyType}]: {valueType} }}";
                    }
                }
    
                return TsGenerator.DefaultMemberTypeFormatter(tsProperty, memberTypeName);
            }
    
  3. Log in to comment