TypeScriptの、はてなふたつの演算子や、はてなドットの演算子は、JavaScriptの新しい構文だった

TypeScript 3.7以降で使える ?? 演算子|| 演算子より厳密に undefinednull を判定してくれるので多用していたが、こいつの名前は何だろうと思って調べてみたところ、Nullish Coalescing Operator、日本語だとNull結合演算子というらしい。

ついでに、 ?.undefined または null な参照の場合に、エラーではなく undefined にしてくれる、RubyでいうところのNull条件演算子は、 Optional Chaining Operatorというらしい。

どちらもECMAScript2020で追加された構文で、主要ブラウザの最新版や、Node.js v14からはJavaScriptでも使えるようになっているのでメモ。

Nullish Coalescing Operator(Null結合演算子)

developer.mozilla.org

左辺が null または undefined の場合は右辺、それ以外の場合は左辺を戻り値として返す。

|| (OR演算子)との違いは、OR演算子は左辺が false として判定される場合は右辺が返るのに対し、 ?? では null または undefined に限定されること。

Falsy (偽値) - MDN Web Docs 用語集: ウェブ関連用語の定義 | MDN

// null, undefinedの場合は、Null結合演算子とOR演算子の違いなし
console.log(null || 'NULL'); // NULL
console.log(null ?? 'NULL'); // NULL

console.log(undefined || 'UNDEFINED'); // UNDEFINED
console.log(undefined ?? 'UNDEFINED'); // UNDEFINED

// 左辺がfalsyな場合、結果が異なる
console.log(false || true); // true
console.log(false ?? true); // false

console.log(0 || 1); // 1
console.log(0 ?? 1); // 0

console.log('' || '空文字'); // 空文字
console.log('' ?? '空文字'); // (何も表示されない)

Optional Chaining Operator(オプショナルチェイニング演算子)

developer.mozilla.org

オブジェクトのプロパティ参照時、 . (ドット表記法、MDNの文中ではチェーン演算子と呼称)を利用すると、オブジェクトが nullundefined の場合は Uncaught TypeError: Cannot read property が発生するが、 ?. を使うと undefined が返る。

プロパティチェーンで利用すると、途中のプロパティのいずれかで undefined が返れば短絡評価されるので、それ以降は実行されない。

ドット表記法だけでなく、ブラケット表記法や配列のインデックスアクセス、関数呼び出しにも使用可能。

const obj = arr = func = undefined;

console.log(obj?.prop); // ドット表記法
console.log(obj?.['expr']); // ブラケット表記法
console.log(arr?.[0]); // 配列のインデックスアクセス
console.log(func?.('args')); // 関数呼び出し

和名について

2020/8/23時点で、Optional Chaining Operatorの日本語ページでは和訳が「オプショナルチェイニング演算子」と記載されている。

翻訳が進めば「Null条件演算子」になるかな? それとも「オプション連鎖演算子」とかだろうか?

と思っていたら、三項演算子のページからは、「オプション連鎖」でリンクが張られていた。

developer.mozilla.org

2022/7/14 追記

結構前からだが、和訳されていた。

Nullish Coalescing Operator は「Null 合体演算子」、Optional Chaining Operator は「オプショナルチェーン (演算子)」となっていた。

振り返り

TypeScriptの Planning ラベルがついたissueに、バージョンごとのTypeScriptでリリースされた機能などがまとまっている。

github.com

演算子の中でも、はてな記号、クエスチョンマークがつくと検索に引っ掛かりづらいので困る。

参考

ECMAScript2020で追加された仕様については、以下にまとまっている。

ics.media

ファンティア(Fantia)からファンクラブに投稿された画像を一括ダウンロードするスクリプトを書いた

知人から、ファンティアのファンクラブの投稿画像を一括でダウンロードしたいと言われた。

ちょっと見てみると、どうも投稿されたページから一括でファイルをダウンロードできず、また投稿ページの画像をクリックして「オリジナルサイズを表示」リンクをクリックしないと、原寸大画像がダウンロードできない模様。

確かに面倒なので、ダウンローダーがないかと調べてみたが、なさそうだったのでPythonで書いてみた。

github.com

ダウンロードはこちら。

使い方はREADME参照。

工夫したところ

「オリジナルサイズを表示」したときの画像ファイル名がUUIDになっていたため、画像ファイルURLから連番を取得して連番で保存するようにした。

これでダウンロードした画像ファイルを、投稿順で閲覧できる。

ただ、どうも投稿内でセクション? が分かれている場合、下のセクションのほうから画像ファイルURLの番号が振られる模様。

単純に投稿ページ内で連番を振るようにしたほうがよかったかも。

残念なところ

Python書きなれていないのと、知人のOKが出た段階で完成としてしまったので、いろいろと残念。

  • コメントやREADMEなど、もろもろ日本語
    • せめてREADMEくらいはなんちゃって英語にしたい
  • スクリプト分割など一切なし
    • setup.py 書いて階層切っておきたい
  • ユニットテストなし。Pythonのテストの書きかたよくわかってない
  • セッションIDをiniファイルに保存しているのは、セキュリティ的にどうなんだ
    • コマンドライン引数にしようとも思ったが、複数回実行を効率化するために目をつぶった
  • 画像ファイル以外、音声ファイルや動画ファイルには未対応、というか未確認
  • 途中でエラーが出た場合の再実行、ページ指定、投稿指定、画像ダウンロード済みの場合のスキップなど未実装
    • 最初にページや投稿を確認し、結果をファイル出力して、そこから画像ダウンロードするようにすればよかった
    • 1ページ目の先頭から実行されるので、途中で止めてしまえばいいといえばいい
  • 投稿のトップ絵をダウンロードしていない
  • 免責事項の書き方

懸念点

現時点でのファンティア利用規約には違反していないと思うが、実際のところどうだろう。

スクリプト内で参照するURLは、すべて公開情報のため、リバースエンジニアリングにはあたらないと思うが...

振り返り

書き終わった後に調べてみたら、同じようなことをPHPでしている人がいらっしゃった...

github.com

プロセス的には、セッションIDやファンクラブIDをコマンドライン引数で取っていて、ほとんど同じ感じ。

試していないけど、こちらのほうが --downloadExisting という、やりたかった true/false で再ダウンロードする/しないを調整できそう。

もうちょいきちんと調べればよかった...

Google Nest Hubで一部のAMラジオが聞けなくなったが、言い方を変えたら聞けるようになった

備忘録。

Google Nest Hubに「TBSラジオを流して」と話しかけてTBSラジオを聞いていたが、2020/8/1になると突然YouTubeの動画が再生されるようになった。

微妙にハマったのでメモ。

状況

Google Nest Hubに「TBSラジオを流して」と音声コマンド入力すると、2020/7/31 18時ごろまではradikoTBSラジオをストリーミングできていた。

2020/8/1 10時現在、同様の音声コマンドを入力すると、YouTubeで「篠田麻里子YouTube Lab」が再生される。

対応

何か問題が起こったのかと思って焦ったが、「TBSラジオ」と音声コマンド入力すると、従来通りTBSラジオをストリーミングできた。

調査

なぜこんなことになったのか気になったので、「ニッポン放送」と「文化放送」で同じように試してみる。

ラジオ局名 局名のみ 「局名 + 流して」、「局名 + を流して」
TBSラジオ radiko YouTubeで「篠田麻里子YouTube Lab」
ニッポン放送 radiko YouTubeで「JAPAN PODCAST AWARDS 2019 授賞式 ダイジェスト」
文化放送 radiko radiko

文化放送はどちらもradikoだが、ニッポン放送TBSラジオと同様にYouTubeが流れた。また、「局名 + 流して」でも「局名 + を流して」と同様の結果となった。

それぞれのYouTubeのチャンネルを見てみる。

TBSラジオニッポン放送は「/channel」だが、文化放送は「/user」となっている。このあたりが、「流して」をつけたときにYouTubeが流れるかの判定に使われているかもしれない。

続いて、TBSラジオニッポン放送、それぞれのチャンネルのホーム画面を見てみる。

f:id:hepokon365:20200801105018p:plain
TBSラジオ - ホーム

f:id:hepokon365:20200801105101p:plain
ニッポン放送 - ホーム

ホーム画面の最上段の再生リストが怪しい。 再生リストも見てみる。

f:id:hepokon365:20200801105559p:plain
TBSラジオ - 再生リスト

f:id:hepokon365:20200801105642p:plain
ニッポン放送 - 再生リスト

断言はできないが、先頭の再生リストが再生されている模様。

振り返り

「~を流して」という音声コマンド入力に対し、公式YouTubeチャンネルがあれば、その再生リストを流すよう音声認識方法が変わったのだろうか。

Google Homeとかでも起こるのかな?

なんだか、「昨日まではあんなに通じ合っていたのに、どうしたんだよ!」みたいな気持ちになってしまった。

Windows10でGitログからチームを可視化するツール、gilotを試してみた

少し前になるが、こちらの gilot の紹介を見て、ちょっと試してみた。

qiita.com

Windows 10だと、環境設定などで少しつまづいたのでメモ。

環境

2020/7/8時点。

Windows 10 Pro/Home 64bit Version 1909, Python 3.8.3, Git 2.27.0

インストール

pip でインストールするが、 Poetryもインストールしておかないとエラーとなる。

pip install git+https://github.com/hirokidaichi/gilot
Collecting git+https://github.com/hirokidaichi/gilot
  (中略)
ERROR: Exception:
Traceback (most recent call last):
  (中略)
ModuleNotFoundError: No module named 'poetry'

Windowsでは、 Poetryのインストールに Microsoft Visual C++ Build Tools が必要。

pip install poetry
    ERROR: Command errored out with exit status 1:
  (中略)
    error: Microsoft Visual C++ 14.0 is required. Get it with "Microsoft Visual C++ Build Tools": https://visualstudio.microsoft.com/downloads/

Microsoft Visual C++ Build Tools は Chocolatey の microsoft-visual-cpp-build-tools でインストールできるので、GitやPythonも含め、まとめてインストールするには以下のコマンドとなる。

cinst git python microsoft-visual-cpp-build-tools

pip install poetry
pip install git+https://github.com/hirokidaichi/gilot

...と思ったら、後述の通り pip install gilot だけでインストール可能。その場合、Poetryは不要なので、以下のコマンドだけでよかった。

cinst git python microsoft-visual-cpp-build-tools
pip install gilot

logコマンド

以下、コマンドはGit Bashで実行する。

gilot log ${Gitリポジトリパス} で、Gitリポジトリのログ解析を行う。解析対象のGitリポジトリはローカルに存在する必要がある模様。

解析結果は、デフォルトでは標準出力に出力されるが、 -o ${CSV_FILE_PATH} でファイル出力可能。

引数としてGitリポジトリのパスを取る。また、デフォルトでは origin/HEAD から master ブランチ相当となるブランチを指定しているが、 --branch ${BRANCH_NAME} で指定可能。実際にリリースや運用されているブランチを指定することが推奨されていた。

# JavaのGradleプロジェクトのため、 build/gilot/ ディレクトリを作成してそこに出力
# 約5,000コミットのGitリポジトリで2分ほどかかった
$ cd <PATH_TO_GIT_REPOSITORY>
$ date && mkdir -p build/gilot/ && gilot log . --branch master -o build/gilot/repo.csv && date
202078日 水曜日 21:31:58
202078日 水曜日 21:33:48

ファイル出力時の注意点

Qiitaの使用例では、 log の実行結果を -o オプションではなく、リダイレクトでファイル出力している。

gilot log REPO_DIR > repo.csv

-o オプションでファイル出力すると、文字コードUTF-8となるが、Git BashでリダイレクトするとShift_JIS(CP932)になる。

Gitコミットのユーザー名がすべてASCIIであれば問題ないかもしれないが、日本語などのマルチバイト文字がユーザー名に含まれると、後述の plot コマンドの入力として渡した場合にエラーが発生するため、 -o オプションでファイル出力しておいたほうが無難と思われる。

$ cd <PATH_TO_GIT_REPOSITORY>
$ mkdir -p build/gilot/ && gilot log . --branch master > build/gilot/repo.csv
$ gilot plot -i build/gilot/repo.csv -o build/gilot/graph.png
Traceback (most recent call last):
  (中略)
UnicodeDecodeError: 'utf-8' codec can't decode byte 0x8e in position 0: invalid start byte

plotコマンド

gilot log の出力結果を、 gilot plot でグラフ画像に変換する。

-i ${CSV_FILE_PATH} で入力となる gilot log の結果ファイルを指定。 -o ${PNG_FILE_PATH} で出力結果のグラフ画像をPNGで保存。

# 画像生成は数秒で実行できる
$ date && gilot plot -i build/gilot/repo.csv -o build/gilot/graph.png && date
202078日 水曜日 21:37:23
202078日 水曜日 21:37:28

# gilot log の出力をパイプで直接渡すことも可能
$ date && mkdir -p build/gilot/ && gilot log . --branch master | gilot plot -o build/gilot/graph.png && date
202078日 水曜日 21:44:11
202078日 水曜日 21:46:04

複数Gitリポジトリの解析結果を結合

チームが複数のGitリポジトリに対して変更を行っている場合、各リポジトリgilot log した結果をまとめて gilot plot できる。

# フロントエンドはGitHub Flow、バックエンドはgit-flowの別々のGitリポジトリで管理している想定
gilot log ${PATH_TO_FRONTEND} --branch master -o repo-frontend.csv
gilot log ${PATH_TO_BACKEND} --branch develop -o repo-backend.csv
gilot plot -i repo*.csv -o graph.png

振り返り

解説記事がもう出ていた。 plot 以外にも、 hotspothotgraph の説明もあり、非常に詳しい。

synamon.hatenablog.com

こちらの記事を見ると、 pip install gilot でもインストールできる模様。

GitHubのREADME.mdにも、2020/7/8段階で pip install gilot は記載があった模様。やはりREADME.mdは読まないとだめだな...

Vuexでよく見る波括弧を使った引数の代入は、分割代入というJavaScriptの新しい構文だった

Vuex の action の第1引数で、 method({ commit }) といった、波括弧を使ったちょっと気持ち悪い記述を見かける。

実際に渡される context から context.commit を抽出しているんだろうと思って何となく使っていたが、正確にはどういう機能なのかと思って調べてみたのでメモ。

Vuex

とりあえず、Vuexのドキュメントを読んでみる。

vuex.vuejs.org

実際にはコードを少しシンプルにするために ES2015 の引数分割束縛(argument destructuring)がよく使われます

とあり、以下へのリンクが張られている。

github.com

ここでは「Destructuring」という名前で紹介されている。

MDN

VuexやVue.jsがどうこうではなく、シンプルにES6(ECMAScript 2015) で追加された仕様の模様。

JavaScriptの仕様であればMDNを検索すればいい、ということでMDN内を「Destructuring」で検索すると、「分割代入」という構文で引っかかった。

developer.mozilla.org

分割代入構文の使い方

基本的な使い方以外にも、規定値の設定などできたため、MDNから使いそうなものを抜粋。

基本的な使い方

代入の左辺が、代入元の変数からどの値を受け取るかを定義する。代入元として、配列またはオブジェクトを利用可能。

Pythonで、タプルやリストを複数の変数に代入するようなイメージ。

// 配列の場合、左辺を[]で囲む
const [a, b] = [1, 2];
console.log(a); // 1
console.log(b); // 2

// オブジェクトの場合、左辺を{}で囲む
const {x, y} = {x: 3, y: 4}
console.log(x); // 3
console.log(y); // 4

規定値の設定

左辺の変数設定時に 変数名 = 規定値 としておくことで、右辺の該当値が undefined の場合は規定値が使用される。

なお、右辺の値が null の場合、きちんと null が代入される。

// 配列の場合
const [a = 1, b = 2, c = 3] = [, undefined, null];
console.log(a); // 1
console.log(b); // 2
console.log(c); // null

// オブジェクトの場合
const {x = 3, y = 4, z = 5} = {x: undefined, z: null};
console.log(x); // 3
console.log(y); // 4
console.log(z); // null

残余部分への変数の代入

左辺の最後の変数の接頭辞として ... を付与すると、残りの右辺の変数がすべてその変数に設定される。

残余引数を利用している模様。

右辺の残りの変数の数にかかわらず、 ... を付与した変数は、配列の場合は配列、オブジェクトの場合はオブジェクトとなる。残りの変数が0であれば、空の配列またはプロパティのないオブジェクトとなる。

また、オブジェクトの場合、 ... を付与した変数と同じ名前のキーが右辺に存在しても、... を付与した変数のオブジェクトのプロパティとして設定された。

// 配列の場合
let a, b;
[a, ...b] = [1, 2, 3];
console.log(b); // Array [2, 3]
[a, ...b] = [1];
console.log(b); // Array []


// オブジェクトの場合
let x, y;
({x, ...y} = {x: 3, y: 4, z: 5}); // 丸括弧で囲まないとエラーとなる
console.log(y); // Object { y: 4, z: 5 }
({x, ...y} = {x: 3});
console.log(y); // Object {  }

異なる名前を持つ変数への代入

オブジェクトの場合、左辺を { 右辺の変数名: 代入する変数名 } とすることで、オブジェクトのプロパティとは異なる名前の変数に代入できる。

規定値も設定可能。

const {a: x, b: y, c: z = 3} = {a: 1, b: 2};
console.log(x); // 1
console.log(y); // 2
console.log(z); // 3

また、右辺の変数名として、変数を使用できる。ただし、変数名をそのまま書くとエラーとなるため、 [] で囲むことに注意。

const targetKey = 'a';
const {[targetKey]: x} = {a: 1};
console.log(x); // 1

余談

Vue.js でいうと、 v-for でも使える模様。ただし、公式ドキュメントには記載がなさそう。

Vue.jsのv-forで分割代入をする - Qiita

振り返り

JavaScript 引数 波括弧」や、「Vue 引数 気持ち悪い」で検索しても引っかからず、なかなか「分割代入」という名前にたどり着けなかった。

名前がわかったので検索すると、もう数年前にはブログやQiitaで記事になっている。

ES6の機能というと、 let, const やアロー関数、Class構文、スプレッド構文、テンプレート文字列、 Promise などを使うことが多く、意識から漏れていたんだろうなぁ。

macOSでGradleを実行してもIDLEプロセス1つだけで進まなくなったら、ファイアウォールでブロックされているかもしれない

新しいMacBookを購入した人から、開発環境構築中に ./gradlew build がエラーになるとの相談を受けた。

地獄のようにハマったのでメモ。

状況

新しいMacBookにて、Gradleを使ったJavaプロジェクトで ./gradlew build を実行すると、IDLEプロセス1つの状態で全く進まない。

Starting a Gradle Daemon, 1 busy Daemon could not be reused, use --status for details
> IDLE

JDKのインストール、設定などは済んでいる。 gradlew が悪いのかとも思い、別途Gradleをインストールして gradle build しても変わらず。 --refresh-dependencies オプションをつけたり、 ./gradlew clean しても同様の結果となった。

./gradlew --status すると、STATUS, INFO ともに UNKNOWN のGradleデーモンがいくつか表示された。

Gradleデーモンを使っているのが問題かとも思い、 --no-daemon オプション付きで実行したがこれも変わらず。

-d --stacktrace オプション付きで実行すると、多少結果が変わり、以下のエラーが出力された。

[DEBUG] [org.gradle.launcher.daemon.client.DaemonClientConnection] thread 1: dispatching class org.gradle.launcher.daemon.protocol.Build
[DEBUG] [org.gradle.internal.remote.internal.inet.SocketConnection] Discarding EOFException: java.io.EOFException
[ERROR] [org.gradle.internal.buildevents.BuildExceptionReporter] 
[ERROR] [org.gradle.internal.buildevents.BuildExceptionReporter] FAILURE: Build failed with an exception.
[ERROR] [org.gradle.internal.buildevents.BuildExceptionReporter] 
[ERROR] [org.gradle.internal.buildevents.BuildExceptionReporter] * What went wrong:
[ERROR] [org.gradle.internal.buildevents.BuildExceptionReporter] The first result from the daemon was empty. Most likely the process died immediately after connection.

原因

エラーメッセージやら試したコマンドやら、正確にどうやって調べたかは覚えていないが、同じ症状の事例をGradle Forumsで見つけることができた。

discuss.gradle.org

I found out, that when disabling my firewall (ESET) the communication between the Gradle Wrapper Main process and the gradle daemon starts working again.

事例と同様、ESET Endpoint Securityを使っているので、試しにファイアウォールを一時停止したところ、問題なくGradleの各タスクが実行されるようになった。

対応

ファイアウォールを停止したままというのは問題なので、設定を「ルール付き自動モード」から「対話モード」に変更。

ネットワークアクセスの度に許可するかのダイアログが表示されるため、そこで許可することでひとまず対応。

ここも先のGradle Forumsでの対応と同じとなった。

Sadly the only advice they gave me so far was to switch my firewall to interactive mode and allow the requests coming from the gradle wrapper.

その後の問題と対応

Gitのブランチを切り替えてから ./gradlew build するとエラーになるという問題が発生。試行錯誤の中で、Gradleキャッシュがおかしな状態となっていた模様。

./gradlew clean build --refresh-dependencies することで、それ以降は再発しなくなった。

振り返り

ESET Endpoint Securityのファイアウォールがひとまずの原因だったが、同じタイミングでMacBookを更新した人もいるものの、そちらでは同様の事例は発生しなかった。

なぜこの問題が発生する/しない端末があるのかといった根本原因がわからず、他の人の端末でも何かの拍子に発生しないかがちょっと怖い。

また、発生端末ではlocalhostへのアクセスでも、初回は必ずダイアログが表示されるため、最初のうちはかなり面倒そうだった。より良い方法もありそうだが、いったんこの対応で作業してもらっている。

結びの言葉も、Gradle Forumsより引用。

I did not find a better solution yet.

追記

後ほど、該当端末でESETの再インストールを行ったところ、再発しなくなった。なんだったんだろう…

JavaクラスからTypeScriptのインターフェース定義を生成するtypescript-generator Maven/Gradleプラグインを試してみた

フロントエンドをTypeScriptで記述することとなり、リクエストやレスポンスで受け渡すデータもTypeScriptで型定義したいという要望がでてきた。

バックエンドはSpring Bootを使ったJavaアプリケーションで、ダブルメンテになると面倒。

JavaのクラスからTypeScriptの型定義を生成できないかと調べてみたら、 typescript-generator というMaven/Gradleプラグインを使ってできたのでメモ。

github.com

環境

typescript-generator Version 2.24.612。バージョン番号の末尾は公開日の月日かな?

プラグインの導入

Gradleから利用した。Mavenでの設定方法や、各パラメータの設定はGitHubのREADME参照。

apply plugin: 'cz.habarta.typescript-generator'

buildscript {
  repositories {
    mavenCentral()
  }
  dependencies {
    classpath 'cz.habarta.typescript-generator:typescript-generator-gradle-plugin:2.24.612'
  }
}

generateTypeScript {
  jsonLibrary = 'jackson2'
  outputKind = 'global'
}

この状態で gradle generateTypeScript を実行すると、 build/typescript-generator/${project.name}.d.ts が出力される。

パラメーターの設定

出力ファイルの形式など、細かく変更可能。詳細は以下。

TypeScript Generator Maven Plugin – typescript-generator:generate

かなり多いので、確認したものを記述。

必須パラメーター

必須パラメーターは2つ。READMEによると、コンパイルされたClassをJSONとして読み込んで変換しているようで、JSONライブラリが必須。

名前 内容
jsonLibrary JSON変換に使用するライブラリ。
jackson1, jackson2, jaxb, gson, jsonb が設定可能。
outputKind 出力するファイルの内容。
global, module (export...), ambientModule (declare module...) を指定。

Java関連パラメーター

名前 内容
classes 出力対象のクラスを完全修飾クラス名で指定。
ネストされたクラスは className$nestedClassName で指定。
classPatterns 出力対象のクラスをglobで指定。 ** はすべての文字、 *.$ を除く文字に一致。
classesImplementingInterfaces 出力対象のクラスを実装インターフェースで指定。
classesWithAnnotations 出力対象のクラスを付与されたアノテーションで指定。
excludeClasses 除外対象のクラスを完全修飾クラス名で指定。
出力対象のクラスにフィールドとして含まれる他のクラスや、実装しているインターフェースも出力されるため、 java.lang.Comparablejava.io.Serializable を指定しておくといい。

jsonLibraryに 'jackson2' を指定している場合、jackson2Configurationなどで設定を調整できそうだが、未確認。

TypeScript関連パラメーター

名前 内容
outputFileType 出力するファイル種別。 declarationFile (.d.ts), implementationFile (.ts) が設定可能。デフォルトは declarationFile
outputFile 出力するファイル名。デフォルトでは build/typescript-generator/ 配下に、プロジェクト名およびoutputFileTypeに応じた拡張子で出力される。JavaプロジェクトとTypeScriptプロジェクトを同じGitリポジトリで管理している場合、リポジトリ管理下に直接出力するのがよさそう。
module outputKindで ambientModule を指定した場合のモジュール名。
namespace 名前空間名。設定していると namespace が出力される。
Not recommended to combine with modules. とあるが、ここでのmodulesは ambientModule のことかな?
umdNamespace outputFileTypeが declarationFile かつoutputKindで module を指定した場合に設定しておくと、UMDモジュール名となる。
mapClasses デフォルトではJavaのclassもinterfaceも、interfaceとして出力されるが、outputFileTypeが implementationFile の場合、 asClasses を指定すると、Javaのclassはclassとして出力される。
generateConstructors true を指定し、かつmapClassesが asClasses の場合、 constructor(data: <自身の型>) としてコンストラクタが出力される。
mapDate java.util.DateDateマッピングされるが、 asNumberasString を指定することで numberstringマッピングする。
mapEnum java.lang.Enum は列挙型の値を文字列としたUnion型にマッピングされるが、 asInlineUnion, asEnum, asNumberBasedEnum を指定して変更可能。
customTypeMappings javaClassName:typescriptType 形式で、JavaのクラスとTypeScriptの型定義をマッピングする。例えば、 java.time.LocalDateTime:string のような設定が可能。
mapPackagesToNamespaces true を指定すると、Javaのパッケージ名がnamespaceとなる。
addTypeNamePrefix 設定すると、出力される型定義に接頭語が付与される。
addTypeNameSuffix 設定すると、出力される定義に接尾語が付与される。
optionalProperties Optional Propertiesとする方法の指定。デフォルトの useSpecifiedAnnotations では後述のoptionalAnnotationsまたはrequiredAnnotationsで指定。 useLibraryDefinition ではJSONライブラリに応じたアノテーションが付与されたフィールド、 all ではすべてのフィールドとなる。
optionalAnnotations optionalPropertiesが useSpecifiedAnnotations の場合にアノテーションを指定すると、アノテーションが付与されたフィールドはOptional Propertiesとなる。requiredAnnotationsとは排他。
requiredAnnotations optionalPropertiesが useSpecifiedAnnotations の場合にアノテーションを指定すると、アノテーションが付与されていないフィールドはOptional Propertiesとなる。optionalAnnotationsとは排他。
optionalPropertiesDeclaration Optional Propertiesの宣言方法。デフォルトの questionMark では ? が使われる。 questionMarkAndNullableType では ? およびnullとのUnion型となる。 その他、 nullableType , nullableAndUndefinableType , undefinableType が指定可能。
sortDeclarations true を指定すると、出力される型定義やプロパティをアルファベット順でソートされる。プロパティの順序が出力の都度変わることがあったため、気にする場合は true にしておくといい。
sortTypeDeclarations true を指定すると、出力される型定義がアルファベット順でソートされる。こちらはプロパティのソートは行わない。

モジュールおよび名前空間については、以下に詳しい。

Modules and Namespaces

その他にも、 optional から始まるパラメーター、および nullable から始まるパラメーターで、プロパティがオプションか、null許容型かなどを指定できる模様。

出力ファイルフォーマット関連パラメーター

デフォルトでは文字列の囲み文字がダブルクォーテーション、インデントが半角スペース4つ。また、先頭にLintの無効化を含めたコメントが3行出力される。

/* tslint:disable */
/* eslint-disable */
// Generated using typescript-generator version ${typescript-generator.version} on ${yyyy-MM-dd HH:mm:ss}.
名前 内容
stringQuotes 文字列の囲み文字を設定。 doubleQuotes , singleQuotes を設定できる。
indentString インデント文字列を設定。Mavenで設定する場合は属性として xml:space="preserve" を指定。
noTslintDisable true を指定すると、 /* tslint:disable */ を抑制。
noEslintDisable true を指定すると、 /* eslint-disable */ を抑制。
noFileComment true を指定すると、 // Generated... コメントを抑制。

改行コードは設定できない模様。Windowsで出力すると改行コードが CRLF になるので、フォーマットをかけたりがやや面倒。同一GitリポジトリJavaとTypeScriptが保存されている場合などは、生成したらESLintやprettierでフォーマットしておくのがよさそう。

REST Client関連パラメーター

JAX-RSまたはSpringのREST Clinetを生成できる。

Springの場合、pluginの依存性に cz.habarta.typescript-generator:typescript-generator-spring を追加する必要がある。

generateSpringApplicationInterface または generateSpringApplicationClient (outputFileTypeに implementationFile 指定時のみ) に true を指定するとSpring RESTクライアントコードが出力されるということだが、試してみたところ以下が出力された。

// generateSpringApplicationInterface = true の場合
export type RestResponse<R> = Promise<R>;

// generateSpringApplicationClient = true の場合
export interface HttpClient {
  request<R>(requestConfig: { method: string; url: string; queryParams?: any; data?: any; copyFn?: (data: R) => R; }): RestResponse<R>;
}

export type RestResponse<R> = Promise<R>;

function uriEncoding(template: TemplateStringsArray, ...substitutions: any[]): string {
  let result = '';
  for (let i = 0; i < substitutions.length; i++) {
    result += template[i];
    result += encodeURIComponent(substitutions[i]);
  }
  result += template[template.length - 1];
  return result;
}

rest から始まるパラメーター、 restOptionsTyperestResponseType でパラメーターやレスポンスの型を変えられる模様。それぞれ AxiosRequestConfigAxiosPromise を指定した例が記載されているが、試せていない。

JavaとTypeScriptの型マッピング

基本的な型は一通りそろっている。以下のWikiに記載あり。

Type Mapping · vojtechhabarta/typescript-generator Wiki · GitHub

Javaの型 TypeScriptの型
文字列(String, char, Character, UUID) string
数値(java.lang.Number実装クラスおよびそのプリミティブ型) number
論理値(booleanおよびBoolean) boolean
java.util.Date Date, mapDate で変更可能
配列またはjava.util.Collection実装クラス 配列
java.util.Map<String, T>実装クラス 文字列をキーとしたObject
java.lang.Enum 列挙型の値をstringとしたUnion型, mapEnum で変更可能

生成した結果としては、以下のようになった。

  • 指定したクラスが別のクラスを参照している場合、参照先クラスが classPatterns などで指定したクラスに含まれなくとも、定義が出力される。
  • 指定したJSON変換ライブラリの設定内容が反映される。例えば、 java.lang.Enum はデフォルトでは列挙型の値が文字列としてマッピングされるが、Jackson2を使用して、 com.fasterxml.jackson.annotation.JsonValue を用いてフィールドの値をJSONの値としてマッピングした場合、TypeScriptの型定義にもフィールドの値が使用される。

注意点

プリミティブ型もオプションとなる

プリミティブ型も、デフォルトではオプションになる。

issueも上がっているが、調整は現時点ではできない模様。

ラッパークラスで宣言して、 requiredAnnotations で指定したアノテーションを付けておくしかなさそう。

2021/3/22 追記

primitivePropertiesRequired というオプションが追加され、必須化できるようになった。

typescript-generatorにプリミティブ型を必須プロパティ(non null)にするオプションが追加されていた - 毎日へっぽこ

RetentionPolicy.CLASS のアノテーション非対応

JVMに読み込まれたクラスからアノテーションを取得しているため、アノテーションを指定するプロパティでは、基本的に RetentionPolicy.RUNTIME しか判定されない(指定はできるが、無視される)。

Kotlinで記述している場合、こちらのissuevar から付与される @org.jetbrains.annotations.Nullable には対応しているようだが、未確認。

val で付与される @org.jetbrains.annotations.NotNull には対応していない模様。他にも、 @lombok.NonNull も使えなかったりする。

対応としては、別途 @javax.validation.constraints.NotNull あたりを付与しておくか、マーカーアノテーションを独自で用意しておくかしかなさそう。

出力例

build.gradleにて、以下のようなパラメーターを設定。Gitプロジェクトが分かれているため、 finalizedBy で改行コードを変換するついでに、Java側のGitのコミットハッシュをコメントとして追加している。

def tsOutputFileName = "${path/to/output}.ts"

generateTypeScript {
  jsonLibrary = 'jackson2'

  // @Builder で生成されるBuilderクラスを対象外とするため、クラス名の接尾語を指定
  classPatterns = [
      'hepokon365.request.**Request',
      'hepokon365.response.**Response'
  ]
  excludeClasses = [
    'java.lang.Comparable',
    'java.io.Serializable'
  ]
  requiredAnnotations = [
    'javax.validation.constraints.NotNull',
  ]

  outputKind = 'module'
  outputFileType = 'implementationFile'
  outputFile = tsOutputFileName
  namespace = 'Endpoint'
  optionalPropertiesDeclaration = 'questionMarkAndNullableType'

  stringQuotes = 'singleQuotes'
  indentString = '  ' // 半角スペース2つ
  noTslintDisable = true // TSLintは使わない
  noEslintDisable = true // 先頭に追加したいので、出力はせずnormalizeTypeScriptで追加
}

task normalizeTypeScript() doLast {
  def hash = 'git rev-parse --verify HEAD'.execute().text.trim()
  def comment = "/* eslint-disable */\n// Generated ${project.name} git commit hash: ${hash}\n"
  tsOutputFile.text = comment + tsOutputFile.text.normalize()
}

generateTypeScript.finalizedBy normalizeTypeScript

以下のJavaクラス3つを記述。

package hepokon365.request;
@lombok.Data
public class ExampleRequest implements java.io.Serializable {
    @javax.validation.constraints.NotNull
    private String stringValue;
    private boolean booleanValue;
    private hepokon365.enums.ExampleEnum enumValue;
}

package hepokon365.response;
@lombok.Value
@lombok.Builder
public class ExampleResponse implements java.io.Serializable {
    @javax.validation.constraints.NotNull
    private java.math.BigDecimal decimalValue;
    private java.util.Date dateValue;
    private java.util.List<String> stringValues;
}

package hepokon365.enums;
public enum ExampleEnum {
    ENUM_ONE, ENUM_TWO, ENUM_THREE;
}

この状態で gradle generateTypeScript を実行すると、以下のファイルが出力される。 classPatterns で指定されていないクラスも、指定されたクラスから参照されていれば出力に含まれる。

/* eslint-disable */
// Generated ${project.name} git commit hash: ${hash}
// Generated using typescript-generator version 2.24.612 on ${yyyy-MM-dd HH:mm:ss}.

export namespace Endpoint {
  export interface ExampleRequest {
    stringValue: string;
    booleanValue?: boolean | null;
    enumValue?: ExampleEnum | null;
  }

  export interface ExampleResponse {
    decimalValue: number;
    dateValue?: Date | null;
    stringValues?: string[] | null;
  }

  export type ExampleEnum = 'ENUM_ONE' | 'ENUM_TWO' | 'ENUM_THREE';
}

振り返り

細々とフォーマット調整などしているのでパラメーターが多く見えるが、単純に使うだけならクラス指定と出力形式だけでよく、かなり便利。

typescript-generator というプラグイン名が、TypeScriptのジェネレータと被っているので検索に引っ掛かりづらいかも、もったいない。

READMEからリンクされている、以下のブログも参考になる。

www.rainerhahnekamp.com

Spring RESTクライアント生成も、設定がうまくできれば便利そうだが、いったんは型生成で使ってみようと思う。