Announce TypedocConverter, a typescript to C# type bindings converter

Hello everyone,
For a long time when someone try to wrap a typescript library to a C# library, he/she has to rewrite all type bindings with C#, it’s tiring and boring. For example, you want to wrap monaco editor with C#, then you have to rewrite all of monaco models defined in monaco.d.ts with C#.

I searched tools like it, and found quicktype may be the best approach so far. However, it has limited typescript support and will throw if there’re unrecognized types.

Therefore, I wrote a converter (written in F#) to solve above problem: TypedocConverter.

TypedocConverter accept input json result from typedoc, and generate C# bindings into a file or a directory.

For example, you have below typescript code in test.ts:

declare namespace test {
  /**
   * The declaration of an enum
   */
  export enum MyEnum {
    A = 0,
    B = 1,
    C = 2,
    D = C
  }

  /**
   * The declaration of an interface
   */
  export interface MyInterface1 {
    /**
     * A method
     */
    testMethod(arg: string, callback: () => void): string;
    /**
     * An event
     * @event
     */
    onTest(listener: (e: MyInterface1) => void): void;
    /**
     * An property
     */
    readonly testProp: string;
  }

  /**
   * Another declaration of an interface
   */
  export interface MyInterface2<T> {
    /**
     * A method
     */
    testMethod(arg: T, callback: () => void): T;
    /**
     * An event
     * @event
     */
    onTest(listener: (e: MyInterface2<T>) => void): void;
    /**
     * An property
     */
    readonly testProp: T;
  }

  /**
   * The declaration of a class
   */
  export class MyClass1<T> implements MyInterface1 {
    /**
     * A method
     */
    testMethod(arg: string, callback: () => void): string;
    /**
     * An event
     * @event
     */
    onTest(listener: (e: MyInterface1) => void): void;
    /**
     * An property
     */
    readonly testProp: string;
    static staticMethod(value: string, isOption?: boolean): UnionStr;
  }

  /**
   * Another declaration of a class
   */
  export class MyClass2<T> implements MyInterface2<T> {
    /**
     * A method
     */
    testMethod(arg: T, callback: () => void): T;
    /**
     * An event
     * @event
     */
    onTest(listener: (e: MyInterface2<T>) => void): void;
    /**
     * An property
     */
    readonly testProp: T;
    static staticMethod(value: string, isOption?: boolean): UnionStr;
  }

  /**
   * The declaration of a type alias
   */
  export type UnionStr = "A" | "B" | "C" | "other";
}

You can firstly generate json schema using typedoc, and then you can use TypedocConverter to generate converted type bindings:

typedoc test.ts --json test.json
TypedocConverter --inputfile test.json --outputfile test.cs

Above commands will generate C# type bindings, and output to test.cs.

Output:

namespace TypedocConverter.Test
{

    /// <summary>
    /// The declaration of an enum
    /// </summary>
    enum MyEnum
    {
        A = 0,
        B = 1,
        C = 2,
        D = 2
    }
}

namespace TypedocConverter.Test
{

    /// <summary>
    /// The declaration of a class
    /// </summary>
    class MyClass1<T> : MyInterface1
    {
        /// <summary>
        /// An property
        /// </summary>
        [Newtonsoft.Json.JsonProperty("testProp", NullValueHandling = Newtonsoft.Json.NullValueHandling.Ignore)]
        public string TestProp { get => throw new System.NotImplementedException(); set => throw new System.NotImplementedException(); }

        /// <summary>
        /// An event
        /// </summary>
        public event System.Action<MyInterface1> OnTest;

        /// <summary>
        /// A method
        /// </summary>
        public string TestMethod(string arg, System.Action callback) => throw new System.NotImplementedException();

        static UnionStr StaticMethod(string value, bool isOption) => throw new System.NotImplementedException();

    }
}

namespace TypedocConverter.Test
{

    /// <summary>
    /// Another declaration of a class
    /// </summary>
    class MyClass2<T> : MyInterface2<T>
    {
        /// <summary>
        /// An property
        /// </summary>
        [Newtonsoft.Json.JsonProperty("testProp", NullValueHandling = Newtonsoft.Json.NullValueHandling.Ignore)]
        public T TestProp { get => throw new System.NotImplementedException(); set => throw new System.NotImplementedException(); }

        /// <summary>
        /// An event
        /// </summary>
        public event System.Action<MyInterface2<T>> OnTest;

        /// <summary>
        /// A method
        /// </summary>
        public T TestMethod(T arg, System.Action callback) => throw new System.NotImplementedException();

        static UnionStr StaticMethod(string value, bool isOption) => throw new System.NotImplementedException();

    }
}

namespace TypedocConverter.Test
{

    /// <summary>
    /// The declaration of an interface
    /// </summary>
    interface MyInterface1
    {
        /// <summary>
        /// An property
        /// </summary>
        [Newtonsoft.Json.JsonProperty("testProp", NullValueHandling = Newtonsoft.Json.NullValueHandling.Ignore)]
        string TestProp { get; set; }

        /// <summary>
        /// An event
        /// </summary>
        event System.Action<MyInterface1> OnTest;

        /// <summary>
        /// A method
        /// </summary>
        string TestMethod(string arg, System.Action callback);

    }
}

namespace TypedocConverter.Test
{

    /// <summary>
    /// Another declaration of an interface
    /// </summary>
    interface MyInterface2<T>
    {
        /// <summary>
        /// An property
        /// </summary>
        [Newtonsoft.Json.JsonProperty("testProp", NullValueHandling = Newtonsoft.Json.NullValueHandling.Ignore)]
        T TestProp { get; set; }

        /// <summary>
        /// An event
        /// </summary>
        event System.Action<MyInterface2<T>> OnTest;

        /// <summary>
        /// A method
        /// </summary>
        T TestMethod(T arg, System.Action callback);

    }
}

namespace TypedocConverter.Test
{

    /// <summary>
    /// The declaration of a type alias
    /// </summary>
    [Newtonsoft.Json.JsonConverter(typeof(UnionStrConverter))]
    enum UnionStr
    {
        ///<summary>
        /// A
        ///</summary>
        A,
        ///<summary>
        /// B
        ///</summary>
        B,
        ///<summary>
        /// C
        ///</summary>
        C,
        ///<summary>
        /// other
        ///</summary>
        Other
    }

    class UnionStrConverter : Newtonsoft.Json.JsonConverter
    {
        public override bool CanConvert(System.Type t) => t == typeof(UnionStr) || t == typeof(UnionStr?);

        public override object ReadJson(Newtonsoft.Json.JsonReader reader, System.Type t, object? existingValue, Newtonsoft.Json.JsonSerializer serializer)
            => reader.TokenType switch
            {
                Newtonsoft.Json.JsonToken.String =>
                    serializer.Deserialize<string>(reader) switch
                    {
                        "A" => UnionStr.A,
                        "B" => UnionStr.B,
                        "C" => UnionStr.C,
                        "other" => UnionStr.Other,
                        _ => throw new System.Exception("Cannot unmarshal type UnionStr")
                    },
                _ => throw new System.Exception("Cannot unmarshal type UnionStr")
            };

        public override void WriteJson(Newtonsoft.Json.JsonWriter writer, object? untypedValue, Newtonsoft.Json.JsonSerializer serializer)
        {
            if (untypedValue is null) { serializer.Serialize(writer, null); return; }
            var value = (UnionStr)untypedValue;
            switch (value)
            {
                case UnionStr.A: serializer.Serialize(writer, "A"); return;
                case UnionStr.B: serializer.Serialize(writer, "B"); return;
                case UnionStr.C: serializer.Serialize(writer, "C"); return;
                case UnionStr.Other: serializer.Serialize(writer, "other"); return;
                default: break;
            }
            throw new System.Exception("Cannot marshal type UnionStr");
        }
    }
}

You can specify prefixed namespace with “–namespace …”, also you can split classes into multiple files with “–splitfiles true”.

Hope that you can enjoy it :smiley:
Feel free to file a issue if you encounter any problem while using TypedocConverter, and PR is warmly welcomed.

.NET Foundation Website | Blog | Projects | Code of Conduct