• 作成:
  • 更新:

wasmを使わずにRustプログラムの構造体に対応したTypeScriptの型定義ファイルを吐き出す

やりたいこと

actix-webでwebアプリケーションを書いていて, フロントエンドとの通信をJSONで行うことにしました. どうせFormを使ってもFormをHTMLに展開できるテンプレートエンジンなどが無いからです.

この小規模のアプリケーションならばPOSTを禁止して全部PUTにしてしまえばCSRF対策考える必要もなくなりますし.

さてJSONをRust側で表す型はstructで書いてSerdeでデシリアライズ出来るようにするとして, 対応するTypeScirptの型を自動生成したくなりますね.

自前で書くのはそこまでたいへんではない量ですが, 変更した時に片方の変更忘れで実行時エラーとかイヤですからね.

それで色々探したのですが, wasm-bindgenを使う方法は対象のRustプロジェクト(が依存しているcrate)がwasm向けにビルドできなかったから使えませんでした. TypeScriptの型定義が欲しいだけなのでJavaScriptコードやWebAssemblyは必要ないけどバインディングを作るのに必要なんですね…

typescript-definitionsのexport-typescriptを使う

そこで typescript-definitions - Cargo: packages for Rust を使います.

これも基本はwasmにまずコンパイルしないと使えないツールなのですが,

typescript-definitions = {version = "0.1.10", features = ["export-typescript"]}

のようにexport-typescriptを有効にするとwasmでコンパイルしなくても型定義を出してくれます.

使い方は公式ドキュメントにも載ってますが, 型定義を作りたい構造体に

#[derive(Debug, Serialize, Deserialize, TypeScriptify)]
#[serde(rename_all = "camelCase")]

を付与します. JSONとして綺麗になるようにcamelCaseにもしておきましょう.

これはコンパイルしているわけではなくsynを使ってパースして出力するだけなので, type aliasなどは認識せず, 簡易な変換を行ってくれるだけです.

それでもBTreeMapなどは認識してオブジェクトにしてくれるぐらいはやってくれますが.

DateTimeなどを使いたい場合は#[ts(ts_type = "string")]を使って誤魔化しましょう.

そしてTypeScriptのコンパイル前にRust側のプログラムを動かして型ファイルを出力する必要があるので, Cargo.tomlを編集してbinを2つに増やしましょう.

[[bin]]
name = "export_typescript"
path = "src/export_typescript.rs"

そして私はとりあえず以下のようにして出力しています. 複数増えてもforループ回せばOKですね.

//! TypeScript向け型定義ファイルを吐き出す

use myapp::page::*;
use std::process::Command;
use typescript_definitions::TypeScriptifyTrait;

fn main() {
    let path = "frontend/RustType.tsx";
    let mut source = Foo::type_script_ify().to_string();
    source.insert_str(0, "/* eslint-disable import/prefer-default-export */\n");
    std::fs::write(path, source.as_bytes()).unwrap();
    Command::new("yarn").args(&["fix"]).status().unwrap();
}