前言
我们经常会遇到这样的事情:有时候我们找到了一个库,但是这个库是用 TypeScript 写的,但是我们想在 C# 调用,于是我们需要设法将原来的 TypeScript 类型声明翻译成 C# 的代码,然后如果是 UI 组件的话,我们需要将其封装到一个 WebView 里面,然后通过 JavaScript 和 C# 的互操作功能来调用该组件的各种方法,支持该组件的各种事件等等。
但是这是一个苦力活,尤其是类型翻译这一步。
这个是我最近在帮助维护一个开源 UWP 项目 monaco-editor-uwp 所需要的,该项目将微软的 monaco 编辑器封装成了 UWP 组件。
然而它的 monaco.d.ts 足足有 1.5 mb,并且 API 经常会变化,如果人工翻译,不仅工作量十分大,还可能会漏掉新的变化,但是如果有一个自动生成器的话,那么人工的工作就会少很多。
目前 GitHub 上面有一个叫做 QuickType 的项目,但是这个项目对 TypeScript 的支持极其有限,仍然停留在 TypeScript 3.2,而且遇到不认识的类型就会报错,比如 DOM 类型等等。
因此我决定手写一个代码生成器 TypedocConverter:https://github.com/hez2010/TypedocConverter
构思
本来是打算从 TypeScript 词法和语义分析开始做的,但是发现有一个叫做 Typedoc 的项目已经帮我们完成了这一步,而且支持输出 JSON schema,那么剩下的事情就简单了:我们只需要将 TypeScript 的 AST 转换成 C# 的 AST,然后再将 AST 还原成代码即可。
那么话不多说,这就开写。
构建 Typescipt AST 类型绑定
借助于 F# 更加强大的类型系统,类型的声明和使用非常简单,并且具有完善的recursive pattern。pattern matching、option types 等支持,这也是该项目选用 F# 而不是 C# 的原因,虽然 C# 也支持这些,也有一定的 FP 能力,但是它还是偏 OOP,写起来会有很多的样板代码,非常的繁琐。
我们将 Typescipt 的类型绑定定义到 Definition.fs 中,这一步直接将 Typedoc 的定义翻译到 F# 即可:
首先是 ReflectionKind 枚举,该枚举表示了 JSON Schema 中各节点的类型:
type ReflectionKind = | Global = 0 | ExternalModule = 1 | Module = 2 | Enum = 4 | EnumMember = 16 | Variable = 32 | Function = 64 | Class = 128 | Interface = 256 | Constructor = 512 | Property = 1024 | Method = 2048 | CallSignature = 4096 | IndexSignature = 8192 | ConstructorSignature = 16384 | Parameter = 32768 | TypeLiteral = 65536 | TypeParameter = 131072 | Accessor = 262144 | GetSignature = 524288 | SetSignature = 1048576 | ObjectLiteral = 2097152 | TypeAlias = 4194304 | Event = 8388608 | Reference = 16777216
然后是类型修饰标志 ReflectionFlags,注意该 record 所有的成员都是 option 的
ReflectionFlags = { IsPrivate: bool option IsProtected: option IsPublic: option IsStatic: option IsExported: option IsExternal: option IsOptional: option IsReset: option HasExportAssignment: option IsConstructorProperty: option IsAbstract: option IsConst: option IsLet: option }
然后到了我们的 Reflection,由于每一种类型的 Reflection 都可以由 ReflectionKind 来区分,因此我选择将所有类型的 Reflection 合并成为一个 record,而不是采用 Union Types,因为后者虽然看上去清晰,但是在实际 parse AST 的时候会需要大量 pattern matching 的代码。
由于部分 records 相互引用,因此我们使用 and
来定义 recursive records。
Reflection = { Id: int Name: string OriginalName: Kind: ReflectionKind KindString: option Flags: ReflectionFlags Parent: Reflection option Comment: Comment option Sources: SourceReference list option Decorators: Decorator option Decorates: Type list option Url: option Anchor: option HasOwnDocument: option CssClasses: option DefaultValue: option Type: Type option TypeParameter: Reflection list option Signatures: Reflection list option IndexSignature: Reflection list option GetSignature: Reflection list option SetSignature: Reflection list option Overwrites: Type option InheritedFrom: Type option ImplementationOf: Type option ExtendedTypes: Type list option ExtendedBy: Type list option ImplementedTypes: Type list option ImplementedBy: Type list option TypeHierarchy: DeclarationHierarchy option Children: Reflection list option Groups: ReflectionGroup list option Categories: ReflectionCategory list option Reflections: Map<,Reflection> option Directory: SourceDirectory option Files: SourceFile list option Readme: option PackageInfo: obj option Parameters: Reflection list option } and DeclarationHierarchy = { Type: Type list Next: DeclarationHierarchy option IsTarget: option } Type = { Type: Id: option Name: option ElementType: Type option Value: option Types: Type list option TypeArguments: Type list option Constraint: Type option Declaration: Reflection option } Decorator = { Name: Type: Type option Arguments: obj option } ReflectionGroup = { Title: Kind: ReflectionKind Children: list CssClasses: option AllChildrenHaveOwnDocument: option AllChildrenAreInherited: option AllChildrenArePrivate: option AllChildrenAreProtectedOrPrivate: option AllChildrenAreExternal: option SomeChildrenAreExported: option Categories: ReflectionCategory list option } ReflectionCategory = { Title: Children: list AllChildrenHaveOwnDocument: SourceDirectory = { Parent: SourceDirectory option Directories: Map< option DirName: option Url: SourceFile = { FullFileName: FileName: Url: option Parent: SourceDirectory option Reflections: Reflection list option Groups: ReflectionGroup list option } SourceReference = { File: SourceFile option FileName: Line: Character: Comment = { ShortText: Text: option Returns: option Tags: CommentTag list option } CommentTag = { TagName: ParentName: }
这样,我们就简单的完成了类型绑定的翻译,接下来要做的就是将 Typedoc 生成的 JSON 反序列化成我们所需要的东西即可。
反序列化
虽然想着好像一切都很顺利,但是实际上 System.Text.Json、Newtonsoft.JSON 等均不支持 F# 的 option types,所需我们还需要一个 JsonConverter 处理 option types。
本项目采用 Newtonsoft.Json,因为 System.Text.Json 目前尚不成熟。得益于 F# 对 OOP 的兼容,我们可以很容易的实现一个 OptionConverter
。
OptionConverter() = inherit JsonConverter() override __.CanConvert(objectType: Type) : = match objectType.IsGenericType with | false -> false | true -> typedefof<_ option> = objectType.GetGenericTypeDefinition() override __.WriteJson(writer: JsonWriter,value: obj,serializer: JsonSerializer) : unit = serializer.Serialize(writer,if isNull value then null else let _,fields = FSharpValue.GetUnionFields(value,value.GetType()) fields.[0] ) __.ReadJson(reader: JsonReader,objectType: Type,_existingValue: obj,serializer: JsonSerializer) : obj = let innerType = objectType.GetGenericArguments().[] value = serializer.Deserialize( reader,1)">if innerType.IsValueType then (typedefof<_ Nullable>).MakeGenericType([|innerType|]) else innerType ) cases = FSharpType.GetUnionCases objectType then FSharpValue.MakeUnion(cases.[0],[||]) else FSharpValue.MakeUnion(cases.[1],[|value|])
这样所有的工作就完成了。
我们可以去 monaco-editor 仓库下载 monaco.d.ts 测试一下我们的 JSON Schema deserializer,可以发现 JSON Sechma 都被正确地反序列化了。
反序列化结果
构建 C# AST 类型
当然,此 "AST" 非彼 AST,我们没有必要其细化到语句层面,因为我们只是要写一个简单的代码生成器,我们只需要构建实体结构即可。
我们将实体结构定义到 Entity.fs 中,在此我们只需支持 interface、class、enum 即可,对于 class 和 interface,我们只需要支持 method、property 和 event 就足够了。
当然,代码中存在泛型的可能,这一点我们也需要考虑。
EntityBodyType = { Type: option InnerTypes: EntityBodyType list } EntityMethod = { Comment: Modifier: list Type: EntityBodyType Name: TypeParameter: list Parameter: EntityBodyType list } EntityProperty = { Comment: list Name: Type: EntityBodyType WithGet: WithSet: IsOptional: InitialValue: option } EntityEvent = { Comment: list DelegateType: EntityBodyType Name: } EntityEnum = { Comment: Value: int64 option } EntityType = | Interface | Class | Enum | StringEnum Entity = { Namespace: Comment: Methods: EntityMethod list Properties: EntityProperty list Events: EntityEvent list Enums: EntityEnum list InheritedFrom: EntityBodyType list Type: EntityType TypeParameter: list Modifier: list }
文档化注释生成器
文档化注释也是少不了的东西,能极大方便开发者后续使用生成的类型绑定,而无需参照原 typescript 类型声明上的注释。
代码很简单,只需要将文本处理成 xml 即可。
let escapeSymbols (text: ) = if isNull text then "" text .Replace("&",&") .Replace(<<>>) let toCommentText (text: else text.Split \n" |> Array.map (fun t -> /// " + escapeSymbols t) |> Array.reduce(fun accu next -> accu + + next) getXmlDocComment (comment: Comment) = let prefix = /// <summary>\n" let suffix = \n/// </summary> summary = match comment.Text with | Some text -> prefix + toCommentText comment.ShortText + toCommentText text + suffix | _ -> match comment.ShortText with | "" -> "" | _ -> prefix + toCommentText comment.ShortText + suffix returns = match comment.Returns with | Some text -> \n/// <returns>\n" + toCommentText text + \n/// </returns>" | _ -> "" summary + returns
类型生成器
Typescript 的类型系统较为灵活,包括 union types、intersect types 等等,这些即使是目前的 C# 8 都不能直接表达,需要等到 C# 9 才行。当然我们可以生成一个 struct 并为其编写隐式转换操作符重载,支持 union types,但是目前尚未实现,我们就先用 union types 中的第一个类型代替,而对于 intersect types,我们姑且先使用 object。
然而 union types 有一个特殊情况:string literals types alias。就是这样的东西:
type Size = "XS" | "S" | "M" | "L" | "XL";
即纯 string 值组合的 type alias,这个我们还是有必要支持的,因为在 typescript 中用的非常广泛。
C# 在没有对应语法的时候要怎么支持呢?很简单,我们创建一个 enum,该 enum 包含该类型中的所有元素,然后我们为其编写 JsonConverter,这样就能确保序列化后,typescript 方能正确识别类型,而在 C# 又有 type sound 的编码体验。
另外,我们需要提供一些常用的类型转换:
-
Array<T>
->T[]
-
Set<T>
->System.Collections.Generic.ISet<T>
-
Map<T>
->System.Collections.Generic.IDictionary<T>
-
Promise<T>
->System.Threading.Tasks.Task<T>
- callbacks ->
System.Func<T...>
,System.Action<T...>
- Tuple 类型
- 其他的数组类型如
Uint32Array
- 对于
<void>
,我们需要解除泛型,即T<void>
->T
那么实现如下:
let rec getType (typeInfo: Type): EntityBodyType = genericType = match typeInfo.Type with | intrinsic" -> match typeInfo.Name with | Some name -> match name with | number" -> { Type = double; InnerTypes = []; Name = None } | booleanboolstringvoid; InnerTypes = []; Name = None } | _ -> { Type = object; InnerTypes = []; Name = None } | _ -> { Type = ; InnerTypes = []; Name = None } | reference" | typeParameterPromiseSystem.Threading.Tasks.TaskSetSystem.Collections.Generic.ISetMapSystem.Collections.Generic.IDictionaryArraySystem.ArrayBigUint64Array"; InnerTypes = [{ Type = ulong; InnerTypes = [ ]; Name = None };]; Name = None }; | Uint32ArrayuintUint16ArrayushortUint8ArraybyteBigInt64ArraylongInt32ArrayintInt16ArrayshortInt8ArraycharRegExp; InnerTypes = []; Name = None }; | x -> { Type = x; InnerTypes = []; Name = None }; | _ -> { Type = arraymatch typeInfo.ElementType with | Some elementType -> { Type = ; InnerTypes = [getType elementType]; Name = None } | _ -> { Type = ; InnerTypes = []; Name = None }]; Name = None } | stringLiteraltuple" -> match typeInfo.Types with | Some innerTypes -> match innerTypes with | [] -> { Type = System.ValueTuple"; InnerTypes = innerTypes |> List.map getType; Name = None } | _ -> { Type = union; InnerTypes = []; Name = None } | _ -> printWarning (Taking only the first type " + innerTypes.[0].Type + for the entire union type.) getType innerTypes.[0] // TODO: generate unions | _ ->{ Type = intersection"; InnerTypes = []; Name = None } TODO: generate intersections | reflectionmatch typeInfo.Declaration with | Some dec -> match dec.Signatures with | Some [signature] -> paras = match signature.Parameters with | Some p -> p |> List.map (fun pi -> match pi.Type with | Some pt -> Some (getType pt) | _ -> None ) |> List.collect (fun x -> match x with | Some s -> [s] | _ -> [] ) | _ -> [] getDelegateParas (paras: EntityBodyType list): EntityBodyType list = match paras with | [x] -> [{ Type = x.Type; InnerTypes = x.InnerTypes; Name = None }] | (front::tails) -> [front] @ getDelegateParas tails | _ -> returnsType = match signature.Type with | Some t -> getType t | _ -> { Type = ; InnerTypes = []; Name = None } typeParas = getDelegateParas paras match typeParas with | [] -> { Type = System.Action; InnerTypes = []; Name = None } | _ -> if returnsType.Type = " then { Type = ; InnerTypes = typeParas; Name = None } else { Type = System.Func; InnerTypes = typeParas @ [returnsType]; Name = None } | _ -> { Type = ; InnerTypes = []; Name = None } | _ -> { Type = ; InnerTypes = []; Name = None } mutable innerTypes = match typeInfo.TypeArguments with | Some args -> getGenericTypeArguments args | _ -> [] if genericType.Type = then with | (front::_) -> if front.Type = " then innerTypes <- [] () | _ -> () () { Type = genericType.Type; Name = None; InnerTypes = if innerTypes = [] then genericType.InnerTypes innerTypes; } getGenericTypeArguments (typeInfos: Type list): EntityBodyType list = typeInfos |> List.map getType and getGenericTypeParameters (nodes: Reflection list) = TODO: generate constaints types = nodes |> List.where(fun x -> x.Kind = ReflectionKind.TypeParameter) |> List.map ( x.Name) types |> List.map (fun x -> {| Type = x; Constraint = "" |})
当然,目前尚不支持生成泛型约束,如果以后有时间的话会考虑添加。
修饰生成器
例如 public
、private
、protected
、static
等等。这一步很简单,直接将 ReflectionFlags 转换一下即可,个人觉得使用 mutable 代码会让代码变得非常不优雅,但是有的时候还是需要用一下的,不然会极大地提高代码的复杂度。
getModifier (flags: ReflectionFlags) = modifier = [] match flags.IsPublic with | Some flag -> if flag then modifier <- modifier |> List.append [ public" ] () | _ ->match flags.IsAbstract abstractmatch flags.IsPrivate privatematch flags.IsProtected protectedmatch flags.IsStatic static () modifier
Enum 生成器
终于到 parse 实体的部分了,我们先从最简单的做起:枚举。 代码很简单,直接将原 AST 中的枚举部分转换一下即可。
let parseEnum (section: ) (node: Reflection): Entity = let values = match node.Children with | Some children -> children |> List.where ( x.Kind = ReflectionKind.EnumMember) | None -> [] { Type = EntityType.Enum; Namespace = if section = "" TypeDocGenerator section; Modifier = getModifier node.Flags; Name = node.Name Comment = match node.Comment with | Some comment -> getXmlDocComment comment | _ -> Methods = []; Properties = []; Events = []; InheritedFrom = []; Enums = values |> List.map (fun x -> comment = match x.Comment with | Some comment -> getXmlDocComment comment | _ -> "" mutable intValue = 0L match x.DefaultValue with ????? | Some value -> if Int64.TryParse(value,&intValue) then { Comment = comment; Name = toPascalCase x.Name; Value = Some intValue; } match getEnumReferencedValue values value x.Name with | Some t -> { Comment = comment; Name = x.Name; Value = Some (int64 t); } | _ -> { Comment = comment; Name = x.Name; Value = None; } | _ -> { Comment = comment; Name = x.Name; Value = None; } ); TypeParameter = [] }
你会注意到一个上面我有一处标了个 ?????
,这是在干什么呢?
其实,TypeScript 的 enum 是 recursive 的,也就意味着定义的时候,一个元素可以引用另一个元素,比如这样:
enum MyEnum {
A = 1,B = 2,C = A
}
这个时候,我们需要查找它引用的枚举值,比如在上面的例子里面,处理 C 的时候,需要将它的值 A 用真实值 1 代替。所以我们还需要一个查找函数:
getEnumReferencedValue (nodes: Reflection list) value name = match nodes |> List.where(fun x -> with | Some v -> v <> value && not (name = x.Name) | _ -> true ) |> List.where( x.Name = value) |> List.tryFind(fun x -> 0 with | Some y -> Int32.TryParse(y,&intValue) | _ -> ) with | Some t -> t.DefaultValue | _ -> None
这样我们的 Enum parser 就完成了。
Interface 和 Class 生成器
下面到了重头戏,interface 和 class 才是类型绑定的关键。
我们的函数签名是这样的:
let parseInterfaceAndClass (section: string) (node: Reflection) (isInterface: bool): Entity = ...
首先我们从 Reflection 节点中查找并生成注释、修饰、名称、泛型参数、继承关系、方法、属性和事件:
comment = with | Some comment -> getXmlDocComment comment | _ -> "" exts = (match node.ExtendedTypes with | Some types -> types |> List.map( getType x) | _ -> []) @ (match node.ImplementedTypes []) genericType = types = match node.TypeParameter with | Some tp -> Some (getGenericTypeParameters tp) | _ -> None match types with | Some result -> result | _ -> [] properties = with | Some children -> if isInterface children |> List.where( x.Kind = ReflectionKind.Property) |> List.where(fun x -> x.InheritedFrom = None) exclude inhreited properties |> List.where(fun x -> x.Overwrites = None) exclude overrites properties else children |> List.where( x.Kind = ReflectionKind.Property) | _ -> events = x.Kind = ReflectionKind.Event) |> List.where( exclude inhreited events |> List.where( exclude overrites events x.Kind = ReflectionKind.Event) | _ -> methods = x.Kind = ReflectionKind.Method) |> List.where( exclude inhreited methods |> List.where( exclude overrites methods x.Kind = ReflectionKind.Method) | _ -> []
有一点要注意,就是对于 interface 来说,子 interface 无需重复父 interface 的成员,因此需要排除。
然后我们直接返回一个 record,代表该节点的实体即可。
{ Type = then EntityType.Interface EntityType.Class; Namespace = TypedocConverter section; Name = node.Name; Comment = comment; Modifier = getModifier node.Flags; InheritedFrom = exts; Methods = methods |> List.map ( fun x -> retType = ( match x.Signatures with | Some signatures -> signatures |> List.where( x.Kind = ReflectionKind.CallSignature) | _ -> []) with | [] -> { Type = ; InnerTypes = []; Name = None } | (front::_) -> match front.Type with | Some typeInfo -> getType typeInfo | _ -> { Type = ; InnerTypes = []; Name = None } typeParameter = with | Some (sigs::_) -> types = match sigs.TypeParameter with | Some tp -> Some (getGenericTypeParameters tp) | _ -> None with | Some result -> result | _ -> [] | _ -> [] |> List.map ( x.Type) parameters = getMethodParameters (with | Some signatures -> signatures |> List.where( x.Kind = ReflectionKind.CallSignature) |> List.map( match x.Parameters with | Some parameters -> parameters |> List.where(fun p -> p.Kind = ReflectionKind.Parameter) | _ -> [] ) |> List.reduce(fun accu next -> accu @ next) | _ -> []) { Comment = with | Some comment -> getXmlDocComment comment | _ -> Modifier = then [] getModifier x.Flags; Type = retType Name = x.Name TypeParameter = typeParameter Parameter = parameters } ); Events = events | paras = with | Some sigs -> sigs |> List.where ( x.Kind = ReflectionKind.Event) |> List.map( x.Parameters) |> List.collect (fun x -> with | Some paras -> paras | _ -> []) | _ -> [] { Name = x.Name; IsOptional = match x.Flags.IsOptional with | Some optional -> optional | _ -> false ; DelegateType = with | (front::_) -> System.Delegate; Name = None; InnerTypes = [] } | _ -> match x.Type ; Name = None; InnerTypes = [] } ; Comment = ; Modifier = getModifier x.Flags; } ); Properties = properties | { Comment = getModifier x.Flags; Name = x.Name Type = with | Some typeInfo -> getType typeInfo | _ -> { Type = ; Name = None; InnerTypes = [] } WithGet = ; WithSet = ; IsOptional = ; InitialValue = with | Some value -> Some value | _ -> None } ); Enums = []; TypeParameter = genericType |> List.map( x.Type); }
注意处理 event 的时候,委托的类型需要特殊处理一下。
Type alias 生诚器
还记得我们最上面说的一种特殊的 union types 吗?这里就是处理纯 string 的 type alias 的。
let parseUnionTypeAlias (section: ) (node: Reflection) (nodes: Type list): Entity list = let notStringLiteral = nodes |> List.tryFind(fun x -> x.Type <> ) enums = match notStringLiteral with | Some _ -> printWarning (Type alias " + node.Name + is not supported.) [] | None -> nodes |> List.collect (fun x -> match x.Value with | Some value -> [{ Name = toPascalCase value Comment = ///<summary>\n" + toCommentText value + \n///</summary> Value = None }] | _ -> [] ) if enums = [] [ { Namespace = section Name = node.Name Comment = with | Some comment -> getXmlDocComment comment | _ -> Methods = [] Events = [] Properties = [] Enums = enums InheritedFrom = [] Type = EntityType.StringEnum TypeParameter = [] Modifier = getModifier node.Flags } ] let parseTypeAlias (section: ) (node: Reflection): Entity list = typeInfo = node.Type match typeInfo with | Some aliasType -> match aliasType.Type match aliasType.Types with | Some types -> parseUnionTypeAlias section node types | _ -> printWarning () [] | _ -> printWarning () [] | _ -> []
组合 Prasers
我们最后将以上 parsers 组合起来就 ojbk 了:
rec parseNode (section: match node.Kind with | ReflectionKind.Global -> with | Some children -> parseNodes section children | _ -> [] | ReflectionKind.Module -> parseNodes (then node.Name else section + . + node.Name) children | _ -> [] | ReflectionKind.ExternalModule -> [] | ReflectionKind.Enum -> [parseEnum section node] | ReflectionKind.Interface -> [parseInterfaceAndClass section node ] | ReflectionKind.Class -> [parseInterfaceAndClass section node ] | ReflectionKind.TypeAlias -> match node.Type parseTypeAlias section node | _ -> [] | _ -> [] parseNodes section (nodes: Reflection list): Entity list = match nodes with | ([ front ]) -> parseNode section front | (front :: tails) -> parseNode section front @ parseNodes section tails | _ -> []
至此,我们的 parse 工作全部搞定,完结撒花~~~
代码生成
有了 C# 的实体类型,代码生成还困难吗?
不过有一点要注意的是,我们需要将名称转换为 Pascal Case,还需要生成 string literals union types 的 JsonConverter。不过这些都是样板代码,非常简单。
这里就不放代码了,感兴趣的同学可以自行去我的 GitHub 仓库查看。
测试效果
原 typescipt 代码:
declare namespace test { /** * The declaration of an enum */ export enum MyEnum { A = 0= 2 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): ; * * An property readonly testProp: string; } * * Another declaration of an interface export interface MyInterface2<T> { testMethod(arg: T,1)">): T; onTest(listener: (e: MyInterface2<T>) => readonly testProp: T; } * * The declaration of a class export class MyClass1<T> implements MyInterface1 { readonly testProp: string; static staticMethod(value: string,isOption?: boolean): UnionStr; } * * Another declaration of a class export class MyClass2<T> implements MyInterface2<T> readonly testProp: T; static staticMethod(value: string,1)">* * The declaration of a type alias export type UnionStr = "A" | "B" | "C" | "other"; }
Typedoc 生成的 JSON 后,将其作为输入,生成 C# 代码:
namespace TypedocConverter.Test { /// <summary> /// The declaration of an enum </summary> enum MyEnum { [Newtonsoft.Json.JsonProperty(A Newtonsoft.Json.NullValueHandling.Ignore)] A = B Newtonsoft.Json.NullValueHandling.Ignore)] B = 1C Newtonsoft.Json.NullValueHandling.Ignore)] C = 2D Newtonsoft.Json.NullValueHandling.Ignore)] D = } } The declaration of a class class MyClass1<T> : MyInterface1 { <summary> An property </summary> [Newtonsoft.Json.JsonProperty(testProp Newtonsoft.Json.NullValueHandling.Ignore)] string TestProp { get => throw new System.NotImplementedException(); set => new System.NotImplementedException(); } event System.Action<MyInterface1> OnTest; string TestMethod(string arg,System.Action callback) => System.NotImplementedException(); static UnionStr StaticMethod(string value,bool isOption) => System.NotImplementedException(); } } Another declaration of a class class MyClass2<T> : MyInterface2<T> { Newtonsoft.Json.NullValueHandling.Ignore)] T TestProp { event System.Action<MyInterface2<T>> OnTest; T TestMethod(T arg,System.Action callback) => The declaration of an interface interface MyInterface1 { get; set; } arg,System.Action callback); } } Another declaration of an interface interface MyInterface2<T> The declaration of a type alias </summary> [Newtonsoft.Json.JsonConverter(typeof(UnionStrConverter))] UnionStr { /// A Newtonsoft.Json.NullValueHandling.Ignore)] A, B Newtonsoft.Json.NullValueHandling.Ignore)] B,1)"> C Newtonsoft.Json.NullValueHandling.Ignore)] C,1)"> other Other Newtonsoft.Json.NullValueHandling.Ignore)] Other } class UnionStrConverter : Newtonsoft.Json.JsonConverter { public override bool CanConvert(System.Type t) => t == typeof(UnionStr) || t == typeof(UnionStr?); object ReadJson(Newtonsoft.Json.JsonReader reader,System.Type t,1)">object? existingValue,Newtonsoft.Json.JsonSerializer serializer) => reader.TokenType switch { Newtonsoft.Json.JsonToken.String => serializer.Deserialize<string>(reader) { " => UnionStr.A,1)"> UnionStr.B,1)"> UnionStr.C,1)"> UnionStr.Other,_ => new System.Exception(Cannot unmarshal type UnionStr) },1)">) }; void WriteJson(Newtonsoft.Json.JsonWriter writer,1)"> untypedValue,Newtonsoft.Json.JsonSerializer serializer) { if (untypedValue is null) { serializer.Serialize(writer,1)">null); return; } var value = (UnionStr)untypedValue; (value) { case UnionStr.A: serializer.Serialize(writer,1)">"); ; case UnionStr.B: serializer.Serialize(writer,1)">case UnionStr.C: serializer.Serialize(writer,1)">case UnionStr.Other: serializer.Serialize(writer,1)">default: break; } Cannot marshal type UnionStr); } } }
后记
有了这个工具后,妈妈再也不用担心我封装 TypeScript 的库了。有了 TypedocConverter,任何 TypeScript 的库都能轻而易举地转换成 C# 的类型绑定,然后进行封装,非常方便。
感谢大家看到这里,最后,欢迎大家使用 TypedocConverter。当然,如果能 star 一波甚至贡献代码,我会非常感谢的!