JavaScriptからTypeScriptへの移行プロジェクトが始動

東川翔氏(以下、東川):「JavaScriptからTypeScriptの移行のプロジェクトに関して気を付けた点」について話していきます。

まず自己紹介です。東川と申します。新卒の2年目で新規旅行アプリの開発をしています。興味のある言語はTypeScriptやPostgreSQLです。最近はクライアントアプリケーションの構築にも興味があって、Next.jsに関してアドベントカレンダーを書いています。

今日お話しする概要ですが、マイクロサービスの1つをJavaScriptからTypeScriptに移行した内容をお話しします。状況ですが、新規の旅行アプリの開発が走っていて、マイクロサービス化された1つを除いて、画面やフロントサーバーのアプリケーションなどはTypeScriptで開発されていたんですが、一番大きなサービスがJavaScriptのままで残っていました。

既存のアプリから移植して開発を続けていたため、もともとのJavaScriptのアプリケーションが残っていたのです。開発を続けていく中で、チームのメンバーから「他はTypeScriptでここだけがJavaScriptだと、このサービスだけ型がなくてツライ」という声が上がってくるようになりました。

このようにしてTypeScriptの移行のプロジェクトが立ち上がりました。メンバー全員に対して「JavaScriptからTypeScriptに移行するかどうか」という質問を投げたんですが、意外にもTypeScript化に対して反対意見は出ませんでした。これはなぜかというと、型のある開発の恩恵をメンバー全員が感じていたからだと思います。

そもそも、なぜTypeScriptにするのかというと、型を付けるコストをはるかに上回るメリットがあるからだと思います。1つ目に、型推論があることによってバグを予防できます。例えば大きなアプリケーションだとこの変数がnullやundefinedになりうるかどうかをいちいち覚えておくのは大変です。またアプリケーションが大きい場合、オブジェクトの中身やオブジェクトのキーなどをいちいち覚えておくのも大変です。

TypeScriptであれば、強力なエディタの補完によってこれを解消できます。また、型は簡易的な仕様書としても機能します。型を裏切るようなコードは、トランスパイルで落ちるため書けません。すなわち型はアプリケーションの実際の挙動と密接に関係した仕様書になっている、ということです。

これは大人数や長期間のプロジェクトでより効果的です。例えば初めてチームにジョインしたメンバーも、型があるので関数の挙動を理解しやすく、開発がスッと始められるという利点がありました。またAPIリクエストを実際にしなくてもレスポンスの型が提供されていれば、クライアントのアプリケーションだけを先行開発することも可能です。

その1 アプリ設計やドメインに強い人に相談する

今回のJavaScriptからTypeScriptの移行はなかなか大変だったんですが、やってよかったと思ったことを5つ紹介します。まず1つ目に「アプリの設計やドメイン知識の強い人に相談をする」ことです。TypeScript化は、実は.jsを.tsにして、出てきた型エラーを潰すだけの作業ではなく、もっと複雑です。

なぜかというと、ある部分に型を付けようとすると、そこを参照している型を付ける必要がありますが、参照先を参照しているまた別のところに型を付ける必要が出てきたりと、順番をきちんと整理しないと型を付ける作業量が膨れ上がります。

これをTypeScript化しやすい単位に切り出して、部分的に型を付けるためにはアプリ全体の理解が不可欠です。目についたファイルからとりあえずTypeScript化するのではなく、きちんとアプリの設計やドメイン知識を勉強して、処理が独立しているAPIを見抜いてTypeScript化するようなプロセスを踏むのが非常に有効だと感じています。

その2 本当にできるか素振りをしてみる

2つ目に、「本当にできるかトライアル移行で試す」のが有効かなと思います。JavaScriptのアプリケーションは、ものによっては数万から数十万行の分量をもつ場合があります。すなわちやってみないとわからないことが多いということです。まずは小さいAPIや小さいページに対してTypeScript化の素振りをします。

素振りをして、その結果から全体を移行する期間と実装量を見積もります。今回の場合、実際にAPIを1本リクエストからレスポンスまでTypeScript化してみました。そこからTypeScript化が実際に可能であること、始めるとしたらここからがよい、あるいはたぶんここが難しいという感覚をつかめました。これは全体をTypeScript化する上で、非常に有効だったと感じています。

その3 共通化された処理には最高品質の型を付ける

3つ目ですが、「一番使われる箇所には最高品質の型を付ける」ことが大事かなと思います。どのアプリケーションでもそうだと思うんですが、アプリにはアプリ全体で使われている横断的な処理があると思います。例えばフォルシアが提供している検索アプリケーションだと、APIリクエストを受け取ってそれをSQL文に変換して、SQLのレスポンスを受け取って、それをJSONのAPIレスポンスに変換するというのが標準的な流れです。

今回私が触ったアプリケーションだと、リクエストパラメータからSQL文への変換の処理は、すべてのAPIをまたいで共通化されている処理でした。このような共通化された処理には最高品質の型を付けなければなりません。

なぜかというと間違った型を付けたり、質の低い型付けをしてしまうとバグの温床になったり、不要な型変換によってコードが見にくくなってしまったりするからです。よく言われるように、ほとんどのバグは一部のヤバい部分によって引き起こされます。頻繁に参照される箇所をこのようなヤバい部分にしてしまうのは、よくありません。

その4 リファクタリングと同時進行をしない

4つ目は逆説的なんですが、「リファクタリングとTypeScript移行を同時に行わない」ということです。TypeScript移行に関しては、対象となるファイルがアプリ全体で非常に広いので、リファクタリングとTypeScript移行を同時にやってしまうと、リグレッションテストが落ちたときに、その原因が移行時に意図せず混入したバグなのか、それともリファクタリングの影響なのか切り分けが非常に難しくなります。

またこちらのほうが大事なのですが、リファクタリングは始めるときりがなくなります。今回変更はアプリ全体に影響する可能性があるので、リファクタリングをし始めるとあれもこれも気になって、いつまでたってもTypeScript移行が終わらないという状況に陥ります。

リファクタリングは大事で、TypeScript移行がリファクタリングの大きなチャンスであることは間違いないんですが、同時にやるのではなくてリファクタリングをあとにやるのがよいのかなと思っています。まずはリファクタリングしたい気持ちをグッとこらえて、移行するときに「ここはリファクタリングができる」とコメントを残していきます。

そのとき、スニペットなどを活用してコメントのprefixを揃えると、あとから検索したり整理したりがしやすくなります。最後に移行が終わったあとで、コメントを刈り取るかたちでリファクタリングをすると、リファクタリングとTypeScriptへの移行が効率良くできるのではないかなと思います。

その5 外部との接続部分で型の答え合わせをする

最後は「外部接続部分を型で縛る」ということです。先ほど紹介した検索のアプリだと、APIリクエストを受け取る部分、SQL文を投げる部分、SQLを受け取る部分、APIレスポンスを設定する部分の4点は自分で型を作成したり、型作成ツールを使ったりする必要がありました。このような外部接続部分は、いわば型の答え合わせです。

APIリクエストからSQL文への変換が自然に導出されるようにするのが大事かなと思います。またAPIのリクエストやレスポンスに関しては、Swaggerなどの型を自動的に生成するツールを使うのが非常に有効でした。

以上、5つを紹介しました。TypeScript移行は非常にコストも大きいですが、メリットもかなりあって、そのメリットはコストを上回ると今回の移行のプロジェクトを通して感じました。この発表が、みなさんにとってTypeScript移行の助けになれば幸いです。ご清聴ありがとうございました。

司会者:ありがとうございました。進め方を考えながら実際に案件に取り組んでいるのがすごくいいなと思いました。

質問が2件来ています。1つ目は「コメントのprefixはどんなものを使っていますか? 「FIXME」「TODO」ぐらいしか使ったことがないんですが」というものなんですが、いかがですか?

東川:そうですね。大事なのはこのTypeScript移行に伴うprefixであるということを示すことが大事かなと思っていて、私の場合だとfixme:TypeScript refactorableというprefixを付けていました。prefixにはいくつか種類があって、例えば引数が5つあるけど、後ろ2つは除けるというような場合や、変数の定義はされているんだけど、実際に後ろでは使っていない場合はremovableとかを付けていったかな。始めにスニペットをVS Codeのsnippets.jsonに定義して、そこから自動的に使っていました。

司会者:別の質問なんですが、「4番のリファクタリングを一緒にやらないについて、始めからそういう想定だったのか、それとも何か問題が起きてそういうふうにしようとしたのか、どっちなんでしょうか」。

東川:実は2番目と関係していて、本当にできるかトライアル移行で試したときにリファクタリングをしようかなと思ってリファクタリングを始めたら、あれもこれも気になってしまってできなくなってしまったという経験がありました。なので、リファクタリングをやめようという方針を決めて取り組みました。

司会者:素振りの重要性がわかりますね。

東川:はい。素振りはすごく有効でした。

司会者:ありがとうございます。もう1つ、「最高品質の型がどういうものか知りたい」という質問が来ています。

東川:曖昧な表現をしてしまって申し訳ないんですが、まずコードの品質を揃えることが重要かなと思います。共通化がされていてジェネリクスがきちんと使われているということが1つです。もう1つはType Assertionの数が少ないことかなと思います。

型の付け方がいびつだったりすると、途中でasなど不要なキャストを挟む必要があるので、そういったものの数が少なくなるのが品質の高い型なのかなと思います。ちょっと曖昧な表現をしてしまって恐縮なんですが、私が言いたいことは、一番使われる箇所は一番気合いを持って臨みましょうということです。

司会者:ありがとうございます。