はじめに
タイムリープTypeScript 〜TypeScript始めたてのあの頃に知っておきたかったこと〜
アドベントカレンダー23日目の記事を担当させていただく@kouchanneです!
TypeScriptしか勝たん
TypeScriptというよりJavaScriptのできることって幅が広いのでとても便利な言語ですよね!
初期の頃は型定義がされていないライブラリが多く、泣く泣くany型であまり恩恵を得ることが少ないというのが多かったですが、最近は型定義がない方が少ないくらいになってきて使い勝手が良くなったのと、自分は元々の入り口がJavaなので、タイプセーフな言語が好きなのだったのもあり今ではどっぷりハマってます。
参加理由
@suin さんの投稿はよく拝見させていただいていて、今回主催のカレンダーを作るということで賛同したいなと思い参加させていただきました。
今回のテーマ
あの頃の自分へ「同じ型定義をあっちにもこっちにも書いていたあの頃のおろかな自分へ」ということで型情報をNode.jsで作成されたAPIとReactやelectronといったクライアントサイドと効率的に共有する方法共有させていただければと思います。
あの頃の自分の過ち
例えば次のような構成の RESTAPI
を使ったシステムがあったときに
サーバー側とクライアントサイド側それぞれに同じ型定義を書いてました。
起こりゆる問題
- APIからの返却値を変更する際、クライアントサイドでは実行するまでエラーに気付けない
- 本来はイコールであるはずのものが、別々に管理することで反映漏れという予期せぬ差分を生み出すことがある
- クライアントとサーバーを分業している場合コミュニケーションのロストで、そのファイルはゴミになる可能性がある
考えた構成
APIが起点となったシステムの型情報をうまいこと共有するための構成を考えました。
抑えておくポイント
- API側ではORMマッパーを使っているので自動生成されるものも考慮して型情報を生成する必要がある
APIは自動生成されるファイルがあるのでその恩恵はできる限り受けたいので、それを維持できる形で次のような仕組みを作りました。
構成仕様
- API側のプロジェクトの中にクライアント側と共有したいファイルを置くサブモジュールを作る
- そのサブモジュール内に型を生成する
tsconfig.types.json
を定義して型生成をする - サブモジュールの中で、
package.json
でバージョンを管理しリポジトリにpush -
npm publish
で生成した型情報をnpm package化する - クライアント側では
npm install @sample-repository/sample-common
で持ってきてimportして利用する
型を生成する際のポイント
型を作る用に次のような tsconfig.types.json
を作成しました
{
"extends": "./tsconfig.json",
"declaration": true,
"include": ["src/entities","src/api-response","src/dtos"],
"exclude": ["test/**/*"],
"compilerOptions": {
"outDir": "sample-common/types"
}
}
"extends": "./tsconfig.json",
ベースのtsconfig
を継承して、作成する
継承する必要が特になければ、なくても良いと思います。
"declaration": true,
これをtrueにすると、コンパイルしたtsファイルの中でexportしているもの全ての型定義ファイルをファイルごとに作成します。
"include": ["src/entities","src/api-response","src/dtos"],
ここでは型を生成するディレクトリを指定します。
すべてを吐き出してしまうと大変なので、domain
みたいなディレクトリを作ってそこに共通化するものをまとめるのが良いと思います。
自分はAPIのレスポンスの型とAPIのリクエストの型を生成対象にしています。
"exclude": ["test/**/*"],
除外するディレクトリを指定します。
"outDir": "sample-common/types"
生成した型を吐き出すディレクトリを指定します。
ここで、上記で定義したサブモジュールを吐き出し先にしてあげることで
生成はAPI側、生成されたファイルのバージョン管理はサブモジュール側といった使い方ができるようになります。
型生成の仕方
上記で tsconfig
を作成したら実際に型を生成するコマンドをnpm script
に追加してあげます。
{
"scripts":{
"generatecommon:typegenerate": "tsc -P tsconfig.types.json",
"generatecommon:cleantype": "rimraf sample-common/types",
"generatecommon": "run-s generatecommon:cleantype generatecommon:typegenerate"
}
}
tsc -P tsconfig.types.json
tsc コマンドに -P
コマンドを設定してあげることで、tsconfigファイルを直接指定することができます。
通常のtsconfig.json
とは別で先程作ったファイルを定義してあげることで型を生成するだけ用のスクリプトを作ることができるようになりました。
またtsc
コマンドに--emitDeclarationOnly
オプションをつけてあげればd.ts
のみ生成することが可能です!
※私は、処理自体も共通化したいものがいくつかあったのでこのオプションを付けずに利用しています。
中途半端にファイルが上書きされないように、rimraf sample-common/types
で毎回ディレクトリをクリーンしてあげることで型を生成した際に常に最新に保てるようにしてあげています。
生成した型をアップする
今までの過程で生成した型をnpmパッケージにアップしてあげます。
型を生成したらsample-common
ディレクトリに移動してあげて、そっちのgitに対してコミットしてあげて npm publish
をしてあげればOKです。
私は、npmではなく、GitHubパッケージとしてnpmパッケージをデプロイしています。
※GitHubでnpmパッケージを上げる方法
あとは、クライアント側でsample-common
を npm install
して利用するだけです!
まとめ
簡単ではありますが、TypeScriptの型定義をプロジェクト内で共有するためのの自分なりの構成案を紹介させていただきました。
今回は型情報に特化していますが、これであれば同じJS内で使う共通処理や定数などを sample-common
の中に書いてあげればリポジトリをまたいで使い回すことができるのでかなり開発体験が上がると思います。
実際にプロダクトに導入してみてのメリットなんですが、API側の修正をしたあとにその内容をクライアント側に反映する際、導入する前は「目検で直した項目に合わせて上げる必要」がありましたが、importしているパッケージをアップデートするだけでどこを直さなきゃいけないかを TypeScript
が文法エラーとして教えてくれるようになったので「アップするまで気づかなかった」みたいなことがなくなったのでかなり開発体験が上がりました。
これは、本番アップして実際のデータがくるまでエラーに気づかなかったみたいなことを事前に防げるのでかなり大きな成果です。
さいごに
今回紹介したものはクライアント・サーバーサイドどちらでも使えるTypescript(JavaScript)
ならではのできることなのでその恩恵をできる限り有効活用していきたいなと思いました。
今回の内容は比較的に簡単にできる割に、期待できる効果が大きいのでクライアント・サーバーサイドどちらもTypeScriptを採用している方は是非試してみてください。