TypeScriptによるアプリケーションの開発環境

このエントリーは pyspa Advent Calendar 2019 の11日目の記事です。昨日は @chezou の「VeinのiOSショートカット複数URL対応しました」でした。

1. はじめに

TypeScriptは大変に素晴らしい言語で、僕の手によくなじむ。そのせいか最近はめっきりTypeScriptばかり書いている。

今回のエントリでは、僕がこの一年くらいの間に磨いたTypeScriptのテンプレートプロジェクトについて説明する。かなり何度も使って必要十分なものだけを含めるようにしている。

しかし、僕の知識の偏りがそのままになっているので、万人に合うというわけではないだろう。

とはいえ、開発環境の初期構築はかなり面倒な作業なので参考にして貰えれば嬉しい。

細かい説明なんかよりもコードを見た方が早いってハードコアな方は、こちらへどうぞ。

1.1. OSとハードウェア

テンプレートプロジェクトの説明をする前に、まずは僕の使っているハードウェアとOSについて説明しておく。

Thinkpad X1 Carbon 2016年モデルにWindows10をインストールしてある。ハードウェアスペックは、こうだ。

  • CPU i7 6600U @ 2.6GHz
  • メモリ 16GB
  • ストレージ SAMSUNG NVMe SSD 950 PRO 512GB

流石に三年も経つと同等のスペックのものがかなり安価に購入できるようになってきていると感じる。

この環境で、PC版のLINEとSlackとVS CodeとGitKrakenを動かしているが十分に快適だ。

1.2. 事前準備

このテンプレートプロジェクトでは、事前に必要なソフトウェアとして、

  • git
  • Node.js
  • yarn
  • Visual Studio Code (VS Code)

が必要だ。それぞれインストールしておいて欲しい。

僕と同じようにOSとしてWindowsを使っているならscoopを使って簡単にインストールできる。

尚、scoopについては以前に書いたので、そちらを参考にどうぞ。cf. ScoopでWindowsにおける開発環境構築を最適化しよう

ついでにgitのGUIクライアントとしては、GitKraken がおすすめだ。

2. テンプレートプロジェクトについて

それでは、早速プロジェクトの内容について説明していこう。

2.1. エディタ

まずは、みんなが大好きなエディタの話題から取り上げるとしよう。

今、TypeScriptの開発環境として一番強力なのはVSCodeだ。

何せVSCode自体がTypeScriptで書かれているので、機能不足を感じたらTypeScriptで拡張が書ける。

加えて、入力補完やシンタックスハイライトも大変に優れている。

一部の機能についてはlanguage server protocol経由で他のエディタでも使えるというのも大変に素晴らしい。

また、開発も非常に活発で毎月のように最新版がリリースされる。リビジョン番号が上がってすぐはバギーで、一週間もしないうちに毎回パッチが降ってくるのがまた可愛い。

マイクロソフトらしく互換性に気を使ったリリースをしてくれるので、お気に入りの拡張がいきなり動かなくなることもほとんどない。

VSCodeといえば拡張だけども、僕は別に拡張を山盛りして使っているわけではない。どんどん新しいものを入れてはいるが、使わないやつはどんどん消している。

なので、僕のローカルで長い時間生き延びた拡張だけをテンプレプロジェクトには推奨拡張として設定してある。

丁度いいので僕のおススメ拡張を軽く説明しておこう。

2.1.1. TSLint

VS CodeのTSLint拡張は、エディタ用に作ったメモリ上のASTを流用してコード検査を行うので、コンピュータ資源を効率的に使える。

なので、動作が非常に高速で快適だ。これについては、後でも取り上げる。

2.1.2. REST Client

ウェブアプリケーションやウェブに対して何か要求を送信するようなアプリケーションを作っているなら、こいつはマジで最高の拡張だ。

UIがシンプル過ぎるので一見すると、とっつきにくさはあるけども、分かってしまえばどうということはない。

どう使うのかを説明する。まずは、.httpとか.restみたいな拡張子のファイルを作って中にHTTPリクエストを書く。HTTPリクエストというのは、こういう感じだ。

GET https://www.google.com/

その行の上にSend Requestというリンクが出てくるので、それをクリックする。

そうするとそのHTTPリクエストが送信されて、結果を隣のタブで見られる。

ただ、それだけのものだ。HTTPヘッダやPOSTボディを付けたりもできる。

最近のHTTPリクエストは、GraphQLみたいに改行を伴うHTTPリクエストやAuthorizationヘッダーをちゃんとつけないといけなかったりで、curlだけだとちょいとつらい。

以前からChrome拡張のPostmanを愛用しているが、REST Clientが非常にお手軽なので気が付いたらこちらばかり使っている。

2.1.3. Regex Previewer

正規表現を使うコードはソフトウェア開発において、ある種の油断というか矛盾というか、もやっとしたものが集約されやすい部分だ。

小規模な正規表現をチラっと使っているうちは特に問題がないのだけども、その小規模というのが曖昧で感じ方に大きな個人差がある。

正直言って、Regex Previewerを使って動作確認しないといけないような正規表現は小規模であるとは言えないだろう。

書いた本人は簡単なつもりで書いていても受け取った人間が、それを簡単だと感じるとは限らない。

そういう時、Regex Previewerはマジで便利だ。二週間前の自分は他人って話もある。

ノリノリで書いた正規表現のどこかに抜け漏れがあるんだけどヤバ過ぎてよく分からない、なんて愉快な事に遭遇したことは無いかな?僕はある。

2.1.4. EditorConfig for VS Code

改行コードとタブやスペースをエディタ関係なく同じ結果になるよう調整できるツールがEditorConfigで、それのVSCode版だ。

特にWindowsユーザとMacユーザが混ざっている開発チームでは、ちゃんと調整しておかないと地獄のようなことが起きるので、こういうツールで統一するのが望ましい。

ただし、改行コードやインデントを何にするかは、EditorConfigが決めてくれる訳ではないので十分に揉めてくれたまえ。

2.1.5. gitignore

VSCodeのエクスプローラ機能には、なぜgitignoreするメニューが標準でついて無いのかよくわからない。

頻繁に使うわけではないけど、無いと困る。これは、そういうちょっとしたツールだ。

2.2. パッケージ管理ツール

パッケージ管理ツールとしては、デフォルトのnpmもひところに比べると随分早くなったように感じる。しかし、やっぱりまだyarnの方がキビキビ動く。

特にプロキシの内側にいるとそれを強く感じる。npmだとそもそもモジュールのダウンロードが終わらないことすらあるんだよね。

最近は、pnpmという新しいやつも出てきたけど、yarnほど早くもないし便利でもないので使っていない。

蛇足だが、yarnはnpm scriptsを短いコマンドで実行できるところも気に入っている。

2.3. モジュールバンドラ

モジュールをくっ付けたり圧縮したりするのに何を使うのか?というのは比較的ややこしい問題だよね。

一番人気なのは恐らくwebpackだろう。ただ、こいつは凄い勢いで破壊的変更が入ってくるのでついていくのが大分しんどい。

二回メジャーバージョンアップに付き合ったが、正直もうやりたくない。はっきり言って、出来ることが同じままで名前を変えるみたいなことを頻繁にやらないで欲しい。
僕らはアプリケーションを開発しているのだから、ビルドスクリプトのメンテナンスに大きな工数をかけたくない。

どうしてもwebpackを使うなら、Next.jsみたいに内部にwebpackを抱え込んでいるツールにお任せしてしまうのが良いんだろうなぁと今は考えている。

設定ファイルをゴリゴリ書かなくてもそれなりに上手くバンドルしてくれるツールとして、Parcelが今はお気に入りだ。

Parcel自体には設定ファイルが無く、設定が必要な時はその依存するツールの設定ファイルを読み込んで使うという動きをするのが大変に優れている。(例えば、TypeScriptをビルドするときには、tsconfig.jsonを勝手に見つけて使う)

webpackに比べるとコード量も少ないしやっていることもシンプルなので、一度分かってしまえばどうということもない。

まぁ、シンプルであってイージーではないので、一度分かるまでが大変なのは間違いない。

適用範囲も広くウェブのフロントエンドから、AWSのLambda関数、バッチ処理と色んなケースで使える。一回分かれば色々使えるというのは、大変にありがたい。

破壊的変更の少ないバージョンアップをしてくれるので、ビルドスクリプト自体を延々とメンテナンスする必要もない。

2.4. Lintツール

TypeScriptで真っ当に使えるLintツールは3つある。

まずは、コンパイラのオプションを記述できるtsconfig.jsonで、新規のプロジェクトでこれに strict: true を必ず設定する。これによって、TypeScriptを使ったプログラミングにおけるメリットを最大限に享受できる。

悩ましいのが、typescript-eslintを使うか、TSLintを使うかだ。

将来的にJavaScriptのLintは、eslintに集約されていくという事には同意する。しかし、それは今すぐという訳ではない。というのが僕の考えだ。

僕がTSLintを使い続けている理由はいくつかある。

2.4.1. tslint-microsoft-contrib が最高すぎる

一番大きい理由はこれだ。僕のテンプレートプロジェクトでは、この巨大なルールセットから不要なものを無効化する形でTSLintのルールを構成してある。

TypeScriptには、歴史的経緯により沢山の落とし穴があり、それらに対する十分な知識を僕は持っていないのだ。

なので、自動的に検出できるプログラミング上のミスは出来る限り沢山見つけてほしい。

例えば、僕はPromiseのインスタンスをawaitし忘れるみたいな単純なミスも結構やってしまう。このルールセットなら即座にLintエラーになるので不要なドはまりを避けられる。

また、tslint-microsoft-contribは宣言の省略を基本的に許していない。つまり、これに従ってコードを書いていると、コード全量の三割から四割くらいが型宣言になってしまうことさえある。

慣れないうちは、冗長に感じるかもしれないがコードベースが大きくなるに従って、型の情報がコードを理解する強力な助けになることを実感できるだろう。

2.4.2. VS CodeのTSLint拡張が最高

VS CodeのTSLint拡張は非常に高速に動作する。Lintツールがエディタ上で迅速に動作するというのは開発作業における必須条件だ。

何故なら、Lintエラーは出力されてから出来る限り短い時間内に対処しなければ、結果的に対処されないことが多いからだ。CIサーバでエラーにしてから対処するのでは遅すぎる。

加えて、僕は自動的に解消できるエラーは全てファイル保存時に解決するという方針でコードを書いている。
セミコロンの自動入力やimport文の並び替え、タブからスペースへの変換などは頻繁に実行されるので、ちょっとでも引っ掛かりがあると、それぞれは短くても積み重なってストレスの原因になる。
なお、ファイルの保存は15秒に1回よりも早いペースで行っている。

TSLint拡張は極めて迅速に動作するが、eslint拡張は同じ速度感で動作しない。

僕としては、Lintエラーは基本的に全て手元で出来る限り早く手元で解消すべきだと考えているので、手元での動作が悪いツールは避けている。
もし、eslint拡張が今のTSLint拡張と同様の速度で動くようになったら、乗り換えるだろう。

2.5. テスティングフレームワーク

テスティングフレームワークとしては、依然としてAVAを使っている。

今からテスティングフレームワークを選ぶなら大方Jestだろう。テスティングフレームワークとして求められる機能が全部入っているのだから選び易い。

好みの問題だが、僕は expect タイプのアサーションメソッドが本当に嫌いだ。これを使ったテストコードは英文のように読めなくもないが、実際には英文ではない。

また、やたらめったら比較のためのメソッドを覚えなければテストコードが書けないのも辛い。

加えて、 describe によるテストの構造化はスローテスト問題の発症を早めると僕は考えている。

TypeScriptで用もないのに関数やラムダ式を延々と入れ子にするようなコードを気楽に書かせるべきではない。

そして、僕がJestを使いたくない決定的な理由は、テストの実行がAVAに比べて数倍遅いからだ。

AVAについては、以前のエントリにも書いたので、気になる方はそちらを呼んで欲しい。cf. Modern JavaScript概観、そしてElectronへ

2.6. 便利ライブラリ

次は、最近気に入ってよく使っている便利なライブラリを紹介しよう。

2.6.1. アプリケーション設定を記述する

アプリケーションの設定を読み出すためのコードを書くのは正直面倒だ。

とはいえ、 process.env から直接読み出してしまうと、全部が文字列になってしまって型システムもへったくれもない。加えて、その設定を使う時まで設定エラーを見つけられない。

要は、設定を構造化して読みやすくまとめた上で、アプリケーション起動時に設定情報が全て正しく構成されているのかチェックしたいのだ。

こういう要件を満たす便利なライブラリが convict だ。型定義ファイルがよく出来ているので型付けされた設定を記述できる。ただ、これ単独だと環境変数の定義が若干面倒なので、dotenv を組み合わせて環境変数への設定をファイルにしている。

convictとdotenvを組み合わせて使う方法については、少し長くなりそうなのでここでは説明しない。もしどうしても説明が欲しいということであれば、twitterなり対面なりで、要望して貰えればエントリを書くかもしれない。

環境変数を積極的に使うやり方は便利だが、少し気を付けて使って欲しい。

例えば、CloudFormationなんかでアプリケーションをデプロイする際には、環境変数の中身をテンプレートに記述しなければいけなくなる。

そういう状況では、環境変数から設定情報を取り出して使うのは望ましくない。Secrets Managerみたいな場所から読んできた値を設定するようにした方がいいだろう。

convictではloadメソッドを使うと、そういう状況に対応できる。

2.6.2. 複雑な非同期処理を簡単に書く

Node.jsのランタイム上で動作するコードにおいて非同期処理を免れるのは非常に難しい。

とはいえ、async/await だけを使ってあらゆる非同期処理を書くのは大分つらい。

例えば、50件ずつバッファリングされたデータが非同期で複数回送信されてくるのを一件ずつ処理する。みたいなコードを想像して欲しい。GitHub APIのPaginationみたいなやつだ。

こういう時に使うべきライブラリとして知られているのはRxJSだ。しかし、RxJSを一たび採用するとあらゆる部分に影響を及ぼす。

RxJSを使ったコードの動作を最適化するには、RxJSを出来る限り広く使わざるを得ないのだ。

そして、RxJSの学習コストは極めて高い。

これに対して、出来る限り少ない学習コストで複雑な非同期処理を記述できるライブラリがstreaming-iterablesだ。RxJSほどのボキャブラリは無いけども、それなりに難しいこともできる。

ライブラリに定義されている関数はgeneratorはふんだんに使っていることを除けば単純なので理解し易い。

そういうわけで、RxJSに大きな学習コストを今すぐにはかけたくないが、複雑な非同期処理を書く必要があるならstreaming-iterablesを試してみて欲しい。

2.6.3. ロギング

ロギングライブラリというのは、作りやすいので氾濫し易い。

ただ、作りやすいこととちゃんと使えるものが作れることには余り関係がない。というか、まともなロギングライブラリを書くのは本当に難しい。

つまり、まともに動くロギングライブラリを見つけるのは難しい。だからといって、自前で実装するのは望ましくない、

というわけで、今の一押しロギングライブラリはpino だ。

こいつは、ログのデフォルト出力がJSONになってるあたりクラウドサービスを組み合わせた運用監視を想定した作りになっており大変に素晴らしい。

いざとなれば手を入れて使える程度に処理はコンパクトにまとまっているので、他のロギングライブラリに乗り換えるのも難しくはない。

少し前までwinstonをせっせと使っていたが、内部で使っているlogformで実装されている奇妙な遅延ロードの仕組みにドはまりして以来使うのをやめた。

なお、その問題に関するIssueはこれ。https://github.com/winstonjs/logform/issues/66

2.7. 開発関連のSaaS

最後に、テンプレートプロジェクトが利用を前提としている開発用SaaSを軽く紹介する。

2.7.1. GitHub Actions

以前はビルドが失敗したOSにSSHやRDP接続できるので、CircleCIやAppVeyorを使っていたが、今はGitHub Actionsに移行しつつある。

移行における最初のモチベーションは、マルチOS対応だ。GitHub Actionsは裏でAzure Pipelinesが動いているので、LinuxとMacとWindowsを気兼ねなく使える。

特に、nodeのランタイムとOSの組み合わせでマトリクスビルドが超簡単に構成できるのには感動した。

LinuxとWindowsの両方でビルドするために、複数のCI用設定ファイルを書いていたのだから、本当にありがたい。

また、イベント毎にワークフローの設定ファイルを分割するという方針も大変に良い。

更に、複数の構成で共通的に使う処理は、Actionという形でモジュール化できるので、ワークフロー定義が肥大化するのを防げるようになっている。

ActionはDockerを使えばなんでも出来るし、気軽に書くならJavaScriptやTypeScriptでも書ける。リポジトリ内にActionを配置して動かせるので泥臭い処理の共通化を気兼ねなくできる。

ワークフロー定義の中で使える変数や、 if 文は少々奇妙なところもあるが、そこは我慢のしどころだ。

つい最近自分のプロジェクトで使うためにTypeScriptで書いたActionはこれだ。このコードを読めば、TypeScriptを使ってActionを作るとどういう感じになるのか概観できるだろう。

2.7.2. Renovate

依存ライブラリを自動的に更新するツールは、僕も以前に作ったことがある。しかし、Renovateはそれらを大きく上回る便利さだ。

特に自動マージ機能や、検査対象ライブラリのフィルタ機能、設定を他のリポジトリから読み出す機能なんかが大変に素晴らしい。

モジュール毎に分けてプルリクエストが飛んでくるというのもマージし易くて良い。

勿論、CIをちゃんとセットアップしておけば、その結果を見てマージするのか判断してくれる。

類似するサービスをいくつか試してはみたが、結局Renovateが一番最高ということになった。

それとは別に、GitHubが提供してくれるManaging security vulnerabilities の機能は有効化している。

加えて、yarn audit をデイリービルドの中で実行している。

3. まとめ

今回は、僕が作ったTypeScriptのテンプレートプロジェクトについて長々と説明してきた。

最近は、ユーザインターフェースを作るようなコードは余り書いていないので、そちらの要素技術には言及できなかったのが少し悔やまれる。

テンプレートプロジェクトには入れていないが、aws-cdkはマジでお気に入りだ。

例えば、@aws/dynamodb-data-mapperでアノテーションしたオブジェクトをそのままaws-cdkのconstructとして扱えるようなちょっとしたコードを書く程度には使い込んでいる。

内容に関する提案や、誤りの指摘、要望などがあるなら、Twitterなどで連絡してくれると大変に嬉しいので気軽にメッセージを送信して欲しい。

TypeScriptに限らずJavaScript関連のフロントエンド界隈は、流れが随分と速いので余り無理せずについていきたい。そういう皆さんの助けに少しでもなれば良いなと考えています。

明日は、@drillbits です。