Hacked together C# to TypeScript Interface Generator

Topics: General
Dec 13, 2012 at 6:28 PM

Hi All,

I threw something quick and dirty together to generate Typescript interface files from C# POCOs.  I used a Razor template and the Razor Engine project as the basis of the code generator: http://razorengine.codeplex.com/

This is basically programming 101 and I know a real code generator could be better but here it is... it's enough to do what I need (for passing POCOs between client and server) and I can add types as I move along to the switch statement in the template:

Usage:

new TypeScriptGenerator().Generate(
    model:
    new TypeScriptGeneratorModel()
    {
        // C# Types you want to output as TypeScript Interfaces
        Types = new List<Type>()
            {
                typeof(Programmer),
                typeof(ProgrammingLanguage)
            }
    },
    templatePath: @"c:\yourpath\TypeSciptInterfaceTemplate.cshtml", 
    outputFile: @"c:\yourpath\GeneratedTypes.d.ts");

Generator:

public class TypeScriptGeneratorModel
{
    public List<Type> Types { get; set; }
}

public class TypeScriptGenerator
{
    public void Generate(TypeScriptGeneratorModel model, string templatePath, string outputFile)
    {
        var template = System.IO.File.ReadAllText(templatePath, Encoding.Default);
        var rendered = Razor.Parse(template, model);
        System.IO.File.WriteAllText(outputFile, rendered);
    }
}

Razor Template (a few loops and a big old switch that can be modified to taste):

@model TypeScriptGeneratorModel
/* Generated at @DateTime.Now */
@functions{

    string getTypesScriptType(string propertyType)
    {
        switch (propertyType)
        {
            case "System.String":
                return "string";
                break;

            case "System.Int32":
                return "number";
                break;

            case "System.Boolean":
                return "bool";
                break;
                    
            case "System.Nullable`1[System.Boolean]":
                return "bool";
                break;
            case "System.Nullable`1[System.Int32]":
                return "number";
                break;
                
            default:
                break;
        }
        return null;
    }
}

@foreach (var t in Model.Types)
{    
<text>interface</text> @t.Name @("{")
<text>
</text> // force a line break
    foreach (var p in t.GetProperties())
    {
        var typeString = getTypesScriptType(p.PropertyType.ToString());
        if (typeString == null)
        {
<text>//@p.Name: of type @p.PropertyType does not have a corresponding typescript type mapping yet</text>
        }
        else
        {
            if (p.PropertyType.ToString().Contains("Nullable"))
            {
                @(string.Format("{0}?: {1}; // {2}", p.Name, typeString, p.PropertyType))
            }
            else
            {
                @(string.Format("{0}: {1}; // {2}", p.Name, typeString, p.PropertyType))
            }
<text>
</text> // force a line break
        }

    }
    
@("}")<text>// @t.Name</text>
    
<text>
        
</text> // force a line break
    
}

Example Entities/POCOs and Example Output

I will just extend the case statement above as needed to add my own types.  The generator will put comments in for types it doesn't know yet.  Here are some examples, with some known property types and other unknown property types.

public class Programmer
{
    public string FirstName { get; set; }
    public string LastName { get; set; }
    public List<ProgrammingLanguage> Languages { get; set; }
    public bool HasUsedTypeScript { get; set; }
}

public class ProgrammingLanguage
{
    public string Name { get; set; }
    public int? Version { get; set; }
}

Output - notice "Languages" is an unknown type, I can just add to the switch statement as needed

/* Generated at 12/13/2012 1:11:50 PM */


interface Programmer{
 FirstName: string; // System.String
 LastName: string; // System.String
 //Languages: of type System.Collections.Generic.List`1[CoreWebSite.Tests.Generator.ProgrammingLanguage] does not have a corresponding typescript type mapping yet
HasUsedTypeScript: bool; // System.Boolean
 }// Programmer

        
 interface ProgrammingLanguage{
 Name: string; // System.String
 Version?: number; // System.Nullable`1[System.Int32]
 }// ProgrammingLanguage

That's all! Good enough for me for now.  Maybe it will be a jump start for anyone here before a real generator comes out :)

Jon

P.S. Use at your own risk I literally wrote this in an hour. Cheers

 

Dec 13, 2012 at 7:12 PM

I hacked something similar too :) for poco serializable classes

Dec 13, 2012 at 7:18 PM
Edited Dec 13, 2012 at 7:18 PM

Haha :) Yeah I guess it comes down to: wait for a tool, hand code the mapping or hack something good enough for now....

Dec 14, 2012 at 8:29 AM

I love self-hacked code generators. :) Wrote one myself, which will generate a TypeScript / Silverlight / .NET4 client for our hosted ASP.NET Web API as a build task (or command line tool). Unfortunately nothing in state that I'd feel acceptable to be released, but it works for us.

Dec 14, 2012 at 2:08 PM

Nice! That sounds quite useful! What strategy did you all use for your generation? StringBuilder, T4 templates or something else?

I like Razor as a templating engine because there's built in intellisense and it's what I use for views already.

It's a little cumbersome at some points (emitting C# like code in a C# driven templating engine) but overall pretty great.

I changed the template to make all of the TypeScript properties optional. (not just the nullable C# ones)

That way if I have a method that takes a parameter of a certain interface I can call it like so without passing all of the properties.

this.model.setProgrammer({
    "Name": "Jon Kragh"
});

I've also been cleaning up the switch statement logic.  The reflection API seems to have a lot more useful properties than it used to back in the day (at least from what I remember).

Jan 23, 2013 at 11:58 AM
Edited Feb 8, 2013 at 2:51 PM
Very useful Jon, I started to write a simple thing in tt, but found this easier as it was ready made - thanks! (I put the whole thing in a single cshtml which I keep in a web app to be run whenever I need it https://gist.github.com/4604497) Edit : I made a tt of it aswell : https://gist.github.com/joeriks/4655063
Jan 23, 2013 at 6:27 PM

Cool! I run mine from a unit test whenever I need it.

I have updated the template to generate knockout view models and some other things.

The nice thing is it's really easy to update as I move along.

Feb 1, 2013 at 12:58 AM
I've implemented a version of this using T4 templates. It generates TypeScript interfaces from POCOs in C# and presumably all CLR languages. It handles collections and relationships to other models, and includes references for them (also does nullable primitives).

View the source on BitBucket.
public class Parent
{
    public string Name { get; set; }
    public bool? IsGoodParent { get; set; }
    public virtual ICollection<Child> Children { get; set; }
}
translates to
///<reference path="Child.d.ts" />
interface Parent {
    Name : string;
    IsGoodParent? : bool;
    Children : Child[];
}
Mar 19, 2013 at 9:07 PM
There is my solution - a tool that uses T4 template inside Visual Studio to generate TypeScript interfaces from .NET classes. It supports most of the current language features including modules, collections and inheritance.

Download NuGet package
View documentation
Mar 21, 2013 at 7:30 AM
Lukas, that looks amazing. The only thing (which is true of my solution too) is that the custom tool must be run to regenerate the classes. It would be so cool if there was a way to generate the classes on build. I was bashing my head up against it for a while but to no avail.
Apr 11, 2013 at 12:00 AM
Edited Apr 11, 2013 at 12:01 AM
My take, < 100 lines of T4 only.

Add it to a VS project, put your classes in where now "ACME" is in and done.

Customise it as you like.

See also http://stackoverflow.com/questions/12957820/how-to-reuse-existing-c-sharp-class-definitions-in-typescript-projects/15936878#15936878
    <#@ template debug="true" hostSpecific="true" language="C#" #>
    <#@ output extension=".ts" #>
    <#@ Assembly Name="System.Core.dll" #>
    <#@ assembly name="$(TargetDir)ACME.Core.dll" #>
    <#@ import namespace="System" #>
    <#@ import namespace="System.Reflection" #>
    <#@ import namespace="System.Collections.Generic" #>
    <#@ import namespace="System.Text" #>
    <#@ import namespace="System.Linq" #>

    <#= Interface<Acme.Bunny>() #>
    <#= Interface<Acme.Duck>() #>
    <#= Interface<Acme.Birdy>() #>
    <#= Enums<Acme.CarrotGrade>() #>
    <#= Interface<Acme.LinkParticle>() #>

    <#+  
        List<Type> knownTypes = new List<Type>();

        string Interface<T>()
        {   
            Type t = typeof(T);     
            var sb = new StringBuilder();
            sb.AppendFormat("interface {0} {{\n", t.Name);
            foreach (var mi in GetInterfaceMembers(t))
            {
                sb.AppendFormat("  {0}: {1};\n", this.ToCamelCase(mi.Name), GetTypeName(mi));
            }
            sb.AppendLine("}");
            knownTypes.Add(t);
            return sb.ToString();
        }

        IEnumerable<MemberInfo> GetInterfaceMembers(Type type)
        {
            return type.GetMembers(BindingFlags.Public | BindingFlags.Instance)
                .Where(mi => mi.MemberType == MemberTypes.Field || mi.MemberType == MemberTypes.Property);
        }

        string ToCamelCase(string s)
        {
            if (s.Length < 2) return s.ToLowerInvariant();
            return char.ToLowerInvariant(s[0]) + s.Substring(1);
        }

        string GetTypeName(MemberInfo mi)
        {
            Type t = (mi is PropertyInfo) ? ((PropertyInfo)mi).PropertyType : ((FieldInfo)mi).FieldType;
            return this.GetTypeName(t);
        }

        string GetTypeName(Type t)
        {
            if(t.IsPrimitive)
            {
                if (t == typeof(bool)) return "bool";
                if (t == typeof(char)) return "string";
                return "number";
            }
            if (t == typeof(decimal)) return "number";            
            if (t == typeof(string)) return "string";
            if (t.IsArray)
            {            
                var at = t.GetElementType();
                return this.GetTypeName(at) + "[]";
            }
            if(typeof (System.Collections.IEnumerable).IsAssignableFrom(t)) 
            {
                var collectionType = t.GetGenericArguments()[0]; // all my enumerables are typed, so there is a generic argument
                return GetTypeName(collectionType) + "[]";
            }            
            if (Nullable.GetUnderlyingType(t) != null)
            {
                return this.GetTypeName(Nullable.GetUnderlyingType(t));
            }
            if(t.IsEnum) return "number";
            if(knownTypes.Contains(t)) return t.Name;
            return "any";
        }

        string Enums<T>() // Enums<>, since Enum<> is not allowed.
        {
            Type t = typeof(T);        
            var sb = new StringBuilder();        
            int[] values = (int[])Enum.GetValues(t);
            sb.AppendLine("var ParticleKind = {");
            foreach(var val in values) 
            {
                var name = Enum.GetName(typeof(ParticleKind), val);
                sb.AppendFormat("{0}: {1},\n", name, val);
            }
            sb.AppendLine("}");
            return sb.ToString();
        }
    #>
Apr 22, 2013 at 10:13 AM
I've written a T4 template specifically for generating interfaces from SignalR Hubs. It's at https://gist.github.com/robfe/4583549