ねののお庭。

かりかりもふもふ。

【C#】 C#の型定義からTypeScriptの型定義を生成するTapperというライブラリ/CLI Toolをリリースしました。

背景

注意) サーバサイドはC#で書く前提です。

こんなご時世なのでサーバとクライアントを通信させない、なんて事は滅多にないでしょう。 その際皆さんは通信の中身というかコンテンツ、要するにWEB APIの場合はhttpのbodyですが、をどのような形で定義/シリアライザして運用しているでしょうか?

おそらく大多数がJSONとしてシリアライズされた文字列を乗っけて通信させていると思います。 めんどくさいのが、どのようにクライアントとサーバ間のシリアライズされたデータの型の整合性を取るか、という事です。プロパティ名をタイポしてシリアライズでこけたりね、あると思います。

JSONを使う場合、もっとも素朴にはサーバ側で定義した型のプロパティ名などをぺちぺち手作業でクライアント側にもコピペして、型も一致するように手動でアノテーションしていくという事でしょう。 ちょっとしたプロトタイプくらいならこれでも全く問題ないと思います。

しかし、実際には大量のAPIが生えてくるので、それを全部手作業でコピペしたりアノテーションしたりするのはあまりにもつらすぎます。また手作業でやる事でプロパティ名のタイポや型のミスマッチなどが発生してきます。 そしてなによりサーバサイドでコードに変更を加えたら、クライアント側のコードも変更しないといけません。 サーバ側でコードに変更を加えたら、クライアント側のコードもそれに追従してほしい。そうする事で、もし修正が必要なのであれば、実行時エラーではなくコンパイル時にエラーを吐いてくれるようになります故。

既存の解決手段

まぁ、辛みを覚えるポイントはみんな似たような所なので、幾つかの解決方法が世に出回っています。以下のようなのツール達を使って、サーバ/クライアントの双方のコードを生成してしまえ、というのがよくある方針かと思います。

  • Protocol BuffersのIDLから
  • JSON Schemaから
  • OpenAPIの仕様定義から

おそらく一番筋がいいのは Protocol Buffers 使う事な気がするんですが、正直.protoを書くのがメンドくさくなりました (C#で型定義したい)。 そしてJSON Schemaは恐ろしく読み辛いです。 OpenAPIからサーバ/クライアントの双方のコードの生成というのもありますが、OpenAPIに沿った仕様定義するのは当然(?)苦痛なんですが、生成されるコードもお気に召しませんでした。 (もちろん、すべて個人的な感想にすぎません。)

IDLというか中間表現というかを書いて、C#やTypeScriptコードを生成するのではなく、C#コードからTypeScriptコードを生成できると嬉しいのです。

そこでNSwagでは、OpenAPIの仕様定義からC#コードを吐き出すのではなく、C#のコードからOpenAPI定義を吐き出し、それを元にTypeScriptのクライアントコードを生成する、という事ができるようになっています。

なんですが、NSwagが生成されるTypeScriptコードはお気に召しませんでした。 なぜならC#の名前空間を無視するので名前衝突が起き、その場合勝手にしれっと型名を変更したりします。また型定義がtype aliasではなくclassとinterfaceとして生成されていたり。 あとNSwagでは型定義だけでなく、クライアントコード(fetchをラップしたやつ)も吐き出されるんですが、JSONでシリアライズする前提だったり、なんだかなぁというポイントが多かったのです。

せっかくC#からTypeScriptのコードを生成するなら、TypeScriptでのプロパティの型がC#のどの型に由来するものなのか、ぱっと分かると嬉しいです。 C#からTypeScriptのコードを生成するとintやfloatなどの数値型は全てnumberにならざるを得ないですし、Guidもstringにならざるを得ないので。 NSwagはC# -> OpenAPI -> TypeScript という具合なので、そのあたりは消し飛んでしまっています。

背景というか前口上がめっちゃ長くなってしまいましたが、そんなこんなで、TapperというC#の型定義からTypeScriptの型定義を生成するライブラリ/CLI Tool及びAnalyzerを作りました。 使い方は非常に簡単で、class, struct, record, enum にAttribute付与してCLI叩くだけ。 Analyzerも入れておけばCLI叩く前にIDE上であれこれ教えてくれるので問題も発生しづらいと思います。

Tapper

コードはGitHubに。 GitHubでスター☆押してくれると私が咽び泣いて喜びます。

github.com

まず、.NET Toolとしてtapperをinstallします。

dotnet tool install --global Tapper.Generator
tapper help

でプロジェクトには以下のパッケージを追加。Tapper.Analyzerは無くても平気なんですが、あると何かとIDEが教えてくれるようになるので便利です。

dotnet add package Tapper.Attributes
dotnet add package Tapper.Analyzer

あとは基本的にTypeScriptにトランスパイルしたいC#の型に、[TranspilationSource]という属性を付与して、

using Tapper;

namespace SampleNamespace;

[TranspilationSource] // <- Add attribute!
public class SampleType
{
    public List<int>? List { get; }
    public int Value { get; }
    public Guid Id { get; }
    public DateTime DateTime { get; }
}

以下のコマンド叩くだけ。

tapper --project path/to/XXX.csproj --output outdir

outdir以下にSampleNamespace.tsというTypeScriptファイルが作成されています。 C#の名前空間毎にファイルが切られているので、名前衝突などは起きません。

出力が以下のような感じ。 JSDocでどの型からトランスパイルされたよ~というのが記述されているので、コーディングの助けになります。

/** Transpied from SampleNamespace.SampleType */
export type SampleType = {
  /** Transpied from System.Collections.Generic.List<int>? */
  List?: number[];
  /** Transpied from int */
  Value: number;
  /** Transpied from System.Guid */
  Id: string;
  /** Transpied from System.DateTime */
  DateTime: (Date | string);
}

C#のプロパティ名そのままTypeScriptにするとPascalCaseになってキモいんだけど、というのであれば、--naming-stylecamelCaseを渡せば期待通りになるでしょう。

tapper --project path/to/Xxx.csproj --output outdir --naming-style camelCase

またシリアライザにJsonじゃくなくてMessagePack使いたいんだが、という場合もあるかと思います。 JsonとMessagePackでそれぞれシリアライザの都合を考えると適切な型がわずかに異なるのですが、それは--serializerMessagePackという値を渡す事で解消できます。

tapper --project path/to/Xxx.csproj --output outdir --serializer MessagePack

あとはGitHub上でREADMEを読んでくれると嬉しいです。

まとめ。

C#の型定義からTypeScriptの型定義を生成するTapeprというライブラリ/CLI Toolを作成しました。

github.com

実はこれ、Tapper.Attributesの中に定義されているTranspilationSourceAttributeを自分のプロジェクトにコピペしてしまえば、完全にゼロ依存で使えるようになっています。

なのでまだ Protocol BuffersやJSON Schema、OpenAPIと心中しておらず、実は手作業でサーバサイトとクライアントサイドの型定義を書いているんだ...という方がいらしゃったら、是非どうでしょうか。いざとなれば依存を増やさずに使えることですし!