Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How to reuse existing C# class definitions in TypeScript projects

There is not currently anything that will map C# to TypeScript. If you have a lot of POCOs or you think they might change often, you could create a converter - something simple along the lines of...

public class MyPoco {
    public string Name { get; set; }
}

To

export class MyPoco {
    public Name: string;
}

There is also a discussion on Codeplex about auto-generating from C#.

Just to keep things updated, TypeLite can generate TypeScript interfaces from C#:

http://type.litesolutions.net/


Web Essentials allow to compile C# files to TypeScript .d.ts files on save. Then you could reference the definitions from your .ts files.

enter image description here


If you use vscode you can use my extension csharp2ts which does exactly that.

You just select the pasted C# code and run the Convert C# to TypeScript command from the command palette enter image description here A conversion example:

public class Person
{
    /// <summary>
    /// Primary key
    /// </summary>
    public int Id { get; set; }

    /// <summary>
    /// Person name
    /// </summary>
    public string Name { get; set; }
}

to

export interface Person
{
    /**Primary key */
    Id : number;

    /**Person name */
    Name : string;
}

TypeLite and T4TSs above both looked good, just picked one, TypeLite, forked it to get support for

  • ValueTypes,
  • Nullables
  • camelCasing (TypeScript root doc uses camels, and this goes too nice together with C#)
  • public fields (love clean and readable POCOs, also makes it easy for the C# Compiler)
  • disable module generation

Then I needed C# interfaces and thought it is time to bake my own thing and wrote a simple T4 script that just does what I need. It also includes Enums. No repo required, just < 100 lines of T4.

Usage
No library, no NuGet, just this plain simple T4 file - use "add item" in Visual Studio and choose any T4 template. Then paste this into the file. Adapt every line with "ACME" in it. For every C# class add a line

<#= Interface<Acme.Duck>() #>

Order matters, any known type will be used in follwing interfaces. If you use only interfaces, the file extension can be .d.ts, for enums you need a .ts file, since a variable is instantiated.

Customisation
Hack the script.

<#@ 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 (string.IsNullOrEmpty(s)) return 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 " + t.Name + " = {");
        foreach(var val in values) 
        {
            var name = Enum.GetName(typeof(T), val);
            sb.AppendFormat("{0}: {1},\n", name, val);
        }
        sb.AppendLine("}");
        return sb.ToString();
    }
#>

The next level of the script will be to create the service interface from the MVC JsonController class.