フロントエンドをTypeScriptで記述することとなり、リクエストやレスポンスで受け渡すデータもTypeScriptで型定義したいという要望がでてきた。
バックエンドはSpring Bootを使ったJavaアプリケーションで、ダブルメンテになると面倒。
JavaのクラスからTypeScriptの型定義を生成できないかと調べてみたら、 typescript-generator
というMaven/Gradleプラグインを使ってできたのでメモ。
環境
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.Comparable や java.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.Date は Date にマッピングされるが、 asNumber や asString を指定することで number や string にマッピングする。 |
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 を指定すると、出力される型定義がアルファベット順でソートされる。こちらはプロパティのソートは行わない。 |
モジュールおよび名前空間については、以下に詳しい。
その他にも、 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
から始まるパラメーター、 restOptionsType
や restResponseType
でパラメーターやレスポンスの型を変えられる模様。それぞれ AxiosRequestConfig
や
AxiosPromise
を指定した例が記載されているが、試せていない。
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で記述している場合、こちらのissueで var
から付与される @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からリンクされている、以下のブログも参考になる。
Spring RESTクライアント生成も、設定がうまくできれば便利そうだが、いったんは型生成で使ってみようと思う。