続・Java と TypeScript をタイプセーフにツナグ

前回のエントリの続きといった感じです。 自分は、今のところ Webアプリは、サーバサイド Java、クライアントサイド TypeScript で作りたいと思っています。 実際 TypeScript を利用していて、タイプセーフになってないと思う部分がありました。 サーバからの Jsonのレスポンスをanyで受け取り、それをそのまま何かしらのテンプレートエンジンに流し込んでたわけですが、それって全然タイプセーフじゃないなと。 で、前回、Java の Entity から TypeScript のクラスを自動生成すれば、ある程度タイプセーフになるんじゃないかと思って、その処理を作ってみたわけです。 今回、ちょっと修正して Gradle のタスクとして呼び出すようにしてみました。 クライアントサイドも Virtual DOM 系のフレームワークの方が、タイプセーフ的になるかもと思い、Mithril.js で Example を作ってみました。 以下の様な構成です。

  • サーバサイド: Spring Boot (Java)
  • ビルド: Gradle
  • クライアントサイド: Mithril.js(TypeScriptで)

例によって単純な足し算・引き算をする Example です。

サーバサイド

package app;

import io.github.chibat.j2ts.annotation.TypeScriptClass;
import lombok.Data;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
@EnableAutoConfiguration
public class CalculateController {

    @RequestMapping("/calc")
    Result calculate(Input input) { // (1)
        Result result = new Result();
        result.add = input.arg1 + input.arg2;
        result.subtract = input.arg1 - input.arg2;
        return result;
    }

    @TypeScriptClass // (2)
    @Data
    public static class Input {
        private Integer arg1;
        private Integer arg2;
    }

    @TypeScriptClass // (2)
    public static class Result {
        public Integer add;
        public Integer subtract;
    }

    public static void main(String[] args) throws Exception {
        SpringApplication.run(CalculateController.class, args);
    }
}

(1)の calculate メソッドでリクエストパラメータを2つ受け取り、足し算・引き算して結果を Json でレスポンスしています。 バリデーション的なものは省略しています。

で、(2)で TypeScriptClass アノテーションをクラスに付けることにより、TypeScript クラスのコードの出力対象としています。

ビルドスクリプト(Gradle)

repositories.jcenter()

:
: 色々省略
:

dependencies {
  compile 'org.springframework.boot:spring-boot-starter-web:1.2.5.RELEASE'
  compile 'org.webjars:mithril:0.2.0'
  compile 'org.projectlombok:lombok:1.16.4'
  compile 'io.github.chibat:j2ts-annotation:0.0.0' // (1)
}

task generateTypeScriptCode(type: JavaExec) {
  configurations{j2ts}
  dependencies {j2ts 'io.github.chibat:j2ts-generator:0.0.0'} // (2)
  classpath = sourceSets.main.runtimeClasspath
  classpath += configurations.j2ts
  main = 'io.github.chibat.j2ts.generator.Main'
  args(
    'app', // (3)
    'src/main/typescript/entity.ts' // (4)
  );
}

(1) TypeScriptClass アノテーションを使用するために、依存を指定しています。

(2) TypeScriptのコードを出力するジェネレータの依存を指定しています。

(3) TypeScriptClass アノテーションが付いたクラスを探す際の対象となるベースパッケージ名を指定します。

(4) 出力する TypeScriptファイル名を指定します。

依存 jar の j2ts は、jcenter にアップしています。

次のコマンドを実行します。

$ gradlew generateTypeScriptCode

ファイル src/main/typescript/entity.ts が出力されます。

module app.CalculateController {
    export class Input {
        arg2: number;
        arg1: number;
    }
    export class Result {
        add: number;
        subtract: number;
    }
}

クライアントサイド

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<title>j2ts - Example</title>
</head>
<body>
<div id="calc-root"></div>
<script src="/webjars/mithril/0.2.0/mithril.min.js"></script>
<script src="/js/generated/entity.js"></script>
<script src="/js/generated/calc.js"></script>
</body>
</html>

mithril.js は、JSで view を書くんで、HTMLファイル側は、<div id="calc-root"></div> って書くくらいで、そこに view が展開される感じです。

次に、TypeScript のコードです。

/// <reference path="typings/mithril/mithril.d.ts" />

import Result = app.CalculateController.Result;
import Input = app.CalculateController.Input;

function esc(value: number | string) {
    return value || value === 0 ? value : '';
}

class Controller {
    arg1 = m.prop<number>(null);
    arg2 = m.prop<number>(null);
    result = new Result();
    request = () => { // (1)
        if (this.arg1() && this.arg2()) {
            var input = new Input();
            input.arg1 = this.arg1();
            input.arg2 = this.arg2();
            m.request<Result>({method: 'GET', url: '/calc', data: input, dataType: 'json'})
                .then((res)=>{this.result = res;});
        }
    }
}

m.module(document.getElementById("calc-root"), {
    controller: () => {
        return new Controller();
    },
    view: (ctrl: Controller) => { // (2)
        return [
            'Arg1',
            m('input',{type: 'text', value: ctrl.arg1(), onchange: m.withAttr("value", ctrl.arg1)}), m('br'),
            'Arg2',
            m('input', {type: 'text', value: ctrl.arg2(), onchange: m.withAttr("value", ctrl.arg2)}), m('br'),
            m('button', {onclick: ctrl.request}, 'Calc'), m('br'),
            'Add: ' + esc(ctrl.result.add), m('br'),
            'Subtract: ' + esc(ctrl.result.subtract), m('br'),
        ];
    }
});

(1)がサーバにリクエストを投げるところで、生成されたInputクラスをリクエスト、Resultクラスをレスポンスとして利用しています。

(2)は、view の構築をしている訳ですが、タグじゃないんで見づらい感はありますが、きっと慣れれば大丈夫です。。 その代わり、タイプセーフ感は増すと思います。

また、Spring MVC だと RPC 的な TypeScript のコードも吐き出せそうだなーとは考えているところです。

Example コードは、github に上げています。 ジェネレータの j2ts は、 jcenter に登録しております。

以上