TypeScript Models Creation via T4 Templates

In light of what I posted yesterday about reducing the amount of TypeScript I write, I wanted to talk about another aspect of reducing the amount of TypeScript code.  Once common task is to create TypeScript models for your Angular application that match what your server expects.  You generate your TypeScript models for your .NET classes via another T4 template.  You can use the approaches mentioned on StackOverflow.  I used the T4 template from the article as a base.  My only suggestion would be to put this template at the root of your solution, then use it as include in the template files that generate the code.  This way you do not need to have multiple version of the template floating around your solution.  I made a small enhancement to it, adding support for nullable types.  The next issue I encountered was the code that typically resided under New function in Angular controllers.  The code is similar to the following:

this.model = {
    Name: "",
    Id: 0,
    IsBillable: false,
    IsFancy: false,
    IsDeleted: false
};

Although there is not much code to write in this case, the problem becomes much bigger when your .NET class contains dozens of properties.  Would it not be nice if we could generate this code, using default values for each type?  We want to use empty string for string properties, zeros for numbers, nulls for nullable properties, etc…  I wanted to expand on model generation and create a T4 template that generates factories for my TypeScript models.  I call them factories because they create instancees of my models.  Here is what I wrote to solve this issue.

<#@ Assembly Name="System.Core.dll" #>
<#@ assembly name="$(TargetDir)Models.dll" #>
<#@ assembly name="$(TargetDir)Interfaces.dll" #>
<#@ assembly name="$(TargetDir)Common.dll" #>
<#@ assembly name="System.ComponentModel.DataAnnotations.dll" #>
<#@ import namespace="System" #>
<#@ import namespace="System.Reflection" #>
<#@ import namespace="System.Collections.Generic" #>
<#@ import namespace="System.Collections" #>
<#@ import namespace="System.Text" #>
<#@ import namespace="System.Linq" #>
<#+ 
    List<Type> knownTypes = new List<Type>();


    string Factory(Type t)
    {
        var sb = new StringBuilder();
        sb.AppendFormat("texport class {0}Factory {{n", t.Name);
        sb.AppendFormat("ttstatic create(): I{0} {{n", t.Name);
        sb.Append("tttreturn");
        AppendLiteralWithDefaults(t, sb, 4);

        sb.AppendLine("tt}");
        sb.AppendLine("t}");
        knownTypes.Add(t);
        return sb.ToString();
    }

    void AppendLiteralWithDefaults(Type t, StringBuilder sb, int numberOfTabs)
    {
        sb.Append(" {n");
        var tabs = string.Empty;
        for (int i = 0; i < numberOfTabs; i++)
        {
            tabs = tabs + "t";
        }
        var members = GetTypeMembers(t).ToList();
        foreach (var mi in members)
        {
            var commaOrNot = ",";
            if (members.Count == members.IndexOf(mi) + 1)
            {
                commaOrNot = "";
            }
            var propertyType = ((PropertyInfo)mi).PropertyType;
            if (Nullable.GetUnderlyingType(propertyType) != null)
            {
                sb.AppendFormat(tabs + "{0}: {1}{2}n", mi.Name, "null", commaOrNot);
            }
            else
            {
                AppendDefaultValue(propertyType, sb, commaOrNot, mi, numberOfTabs);
            }
        }
        sb.Append(tabs.Substring(0, numberOfTabs - 1) + "}");
           
        if (numberOfTabs == 4)
        {
            sb.AppendLine(";");
        }
        else
        {
            sb.AppendLine("");
        }
    }
    void AppendDefaultValue(Type type, StringBuilder sb, string commaOrNot, MemberInfo mi, int numberOfTabs)
    {
        string value = null;
        var tabs = string.Empty;
        for (int i = 0; i < numberOfTabs; i++)
        {
            tabs = tabs + "t";
        }
        if (type == typeof(int) || type == typeof(double) || type == typeof(decimal))
        {
            value = "0";
        }
        else if (type == typeof(string))
        {
            value = """";
        }
        else if (type == typeof(DateTime))
        {
            value = "null";
        }
        else if (type == typeof(bool))
        {
            value = "false";
        }
        else if (typeof(IEnumerable).IsAssignableFrom(type))
        {
            value = "[]";
        }

        else if (type.IsEnum)
        {
            value = "null";
        }
        if (value != null)
        {
            sb.AppendFormat(tabs + "{0}: {1}{2}n", mi.Name, value, commaOrNot);
        }
        else
        {
            knownTypes.Add(type);
            sb.AppendFormat(tabs + "{0}:", mi.Name);
            AppendLiteralWithDefaults(type, sb, numberOfTabs + 1);
        }
    }

    string Factory<T>()
    {
        Type t = typeof(T);
        return Factory(t);
    }
   
    IEnumerable<MemberInfo> GetTypeMembers(Type type)
    {
        return type.GetMembers(BindingFlags.Public | BindingFlags.Instance)
            .Where(mi => mi.MemberType == MemberTypes.Field || mi.MemberType == MemberTypes.Property);
    }
   
#>

You can take this code and paste it into a new t4 file and place it in a solution folder.  You notice that at the top I include all assemblies that I use in my solution to avoid including them in each tt file that is using this template.  The main method is Factory<T>.  This method takes a single type that I want to generate a factory for.  This code creates new TypeScript class with a single create static method.  This method runs through all the properties of the class.  It figures out simple properties and creates defaults for them, based on property type.  For complex properties this method recursively calls the same code, generating object literal for each complex type property.  At the end I end up with the same code as I shown in the beginning of the article.

To use this template inside another tt file, I just need to make new file look like the following:.

<#@ include file="........FactoriesTemplate.t4"#>
<#@ template debug="true" hostSpecific="true" language="C#" #>
<#@ output extension="ts" #>
module app.setup.factories {
    import IMyClass = app.setup.models.IMyClass;
<#= Factory<Models.Setup.MyClass>() #>
}

The code above assumes that there is a file that contains IMyClass interface / model definition.  I manually add a line that imports this type.  You can play around and use conventions to remove this requirement.  Once I run this tt file / template, the code looks something like the following.


module app.setup.factories {
    import IMyClass = app.setup.models.IMyClass;
    export class MyClassFactory {         static create(): IMyClass{             return {                 Id: 0,                 Name: "",                 IsActive: false,                 IsDeleted: false,                 Children: []             };
        }
    }

}

I have been using T4 templates a lot more in the last few years.  I find that they have many usages, and if used properly, save a lot of time.  Not only that I type less, but I also make fewer mistakes.

Thanks and enjoy.

2 Comments

  1. Any thoughts to Ecmascript 6 and the needs of typescript? I have been digging and the main reason I was looking at Typescript was the strong typing, but it seems this will come implicitly with EC6 when it hits the ground in 7 months with many browsers adding pre-release support. Am I missing the point of Typescript thinking most of these concerns are addressed now in PO(js)O?

  2. TypeScript is all about tooling, not the script. If ECMA 6 tooling is just as good, then maybe TypeScript is not necessary. However, you have to have requirements for your app that are pinned to ES6. Probably not going to be the case for most apps for quite some time. IMHO.

Leave a Reply

Your email address will not be published. Required fields are marked *