新規のプロジェクトはnoImplicitAnyを有効にする
鈴木僚太氏(以下、鈴木):ではここから本日の3つ目の話題に移りたいと思います。これが本日のメインディッシュですね。LINE証券のコードベースでnoImplicitAnyを徐々に有効化していった話です。
まずnoImplicitAnyとは何かという話を少しさせてください。一言で言うと、TypeScriptコードに対して厳しいチェックを有効にするコンパイラオプションです。これはいろいろなチェックがあるんですが、その中でも特に大きいのは関数の引数の型を書かないとコンパイルエラーになる機能です。
今画面に出ているコードは3つの関数引数に赤線が引かれていますが、これは関数の宣言であるにもかかわらず、引数に型注釈が書いていないからです。noImplicitAnyをオフにしている場合には、引数の型を書かなくてもコンパイルエラーになることはありません。型がわからないので、これらの引数の型はanyとなります。
私からのアドバイスとしては、新規のプロジェクトはぜひnoImplicitAnyを有効にしてくださいということです。現在TypeScriptコンパイラに対するconfigは、主にtsconfig.jsonを使って行うのが大変主流となっていますが、このtsconfig.jsonを生成するコマンドである「tsc --init」では、最初からnoImplicitAnyが有効なconfigファイルが生成されます。
この生成されたtsconfig.jsonですが、具体的にnoImplicitAny trueと書いてあるわけではなく、strictコンパイラオプションをtrueにするように書かれています。
strictコンパイラオプションは、TypeScriptに存在するさまざまな厳しいチェックのコンパイラオプションをまとめて有効にする、大変優れたコンパイラオプションです。新しいプロジェクトで、今strictをtrueにしない理由はありませんので、ぜひ最初から生成されたtsconfig.jsonに従って、strictがtrueな環境でTypeScriptコードを書いていただければと思います。
any型は非常に危険
さて、今この画像で赤線が引かれているnoImplicitAnyによるコンパイルエラーは、それがないと関数引数の型がanyになってしまうというものでした。このanyは何なのかについても少しだけ話をさせてください。
引数の型がanyになるのは何が問題なのかと申しますと、一言で言うとコードの型安全性が壊滅的な状態になってしまいます。any型の値は、なにをしてもコンパイルエラーがほとんど発生しない値です。
普通TypeScriptのコンパイルエラーは間違ったコード、型が違うコード、あるいはこのまま走らせるとランタイムエラーが起きてしまうようなコードを事前に検出してコンパイルエラーを出してくれる仕組みです。
any型は言わば、この値に対してはなにをしてもいいというようなことを意味する型となっていまして、TypeScriptはany型を発見すると、それに対してたとえどんなに危険なコードがあったとしても、一切無視する。間違ったコードを書いても、決してコンパイルエラーを出さないような挙動をしてしまいます。
このように、any型は非常に危険な型となっています。さらにany型のもう1つの特徴として、any型は伝染することが挙げられます。any型の値に対して、例えばプロパティをアクセスするとか、そういった別の計算を行った場合、その計算結果もまたany型となってしまいます。
このように、any型からは別のany型の値が発生しがちです。またany型が入った時点で、プログラムのロジックは、間違っていてもTypeScriptがコンパイルエラーを発生させてくれない状況となりますので、any型が紛れ込んだ時点で、その関数のロジックはすべて信頼できないと言っても過言ではありません。
noImplicitAnyというTypeScriptのコンパイラオプション、これが無効だと、このように大変危険なany型が、anyと明示的に書かなくても発生してしまいます。noImplicitAnyが無効の状態では、なにも意識しないうちに関数全体が非常に危険な状態に陥ってしまいます。
型安全性にとっては、まさにany型の存在はDisaster、災害とでも言うべきものかと思います。なので、このようなany型が、明示的にanyと書かなくても発生してしまうことを防ぐために、ぜひnoImplicitAnyコンパイラオプションは有効にしておきましょう。
noImplicitAny無効のまま開発を進めていた
さて、ここでLINE証券の開発の話に戻りますが、我々LINE証券のフロントエンドが以前に抱えていた問題、それはまさにTypeScriptのnoImplicitAnyコンパイラオプションが有効になっていなかった、無効の状態で開発を進めてしまっていたという問題でした。
私が気づいたときにはもう手遅れと言いますか、noImplicitAnyがオフの状態で数万行のTypeScriptコードがすでに書かれていました。もちろんこれを直して、すべてのコンパイルエラーを取り去ってnoImplicitAnyを有効にしたいと思ったんですが、最初に申し上げたとおり、我々には新しい案件が次から次へと降ってきますので、noImplicitAnyを有効に直すというタスクに対してまとまった時間を取れませんでした。
それに加えて、仮にnoImplicitAnyを有効化するための時間が取れたとしても、それを直したあとには直したコードがちゃんと正しく動作することを確認するQAのステップが必要になります。我々は、間違ってもサービスを止めることが許されない立場ですので、このQAなしにサービスをリリースすることは考えられません。
noImplicitAnyを有効化するということはサービスのコード全体に手を入れることですので、非常にQAのコストも高くなってしまいます。もし仮に全部コードを直してnoImplicitAnyの有効化を達成したとしても、それをリリースするためのQAができない状況になっていました。
こういった問題があったわけですが、ではそもそもなぜ我々は最初からnoImplicitAnyを有効にしていなかったのかが気になります。というのも、実は我々はこのプロジェクトの最初からTypeScriptを使用していたんです。ということは、最初からnoImplicitAnyを有効化した状態でスタートできたはずです。
それにもかかわらず、我々はnoImplicitAnyを有効にしていなかった理由、それは端的に言ってしまうと、ほかのプロジェクトからコピーされたコードがあったからです。ほかのプロジェクト、つまりLINE証券を開発するメンバーが前に所属していたプロジェクトは、TypeScriptが入っていないJavaScriptのプロジェクトでした。
そのプロジェクトから一部のコードを拝借したことによって、TypeScriptに対応していないJavaScriptのコードが紛れ込んでしまいました。JavaScriptのコードは当然ながら関数の引数に型が書いてあったりはしません。ですので、noImplicitAnyが有効の状態だとコンパイルエラーとなってしまいます。
我々はそこで立ち止まって、最初にnoImplicitAnyを有効にしてからプロジェクトをスタートさせるという選択肢もありましたが、当初我々のプロジェクトメンバーはTypeScriptの経験者が少ないこともあって、TypeScriptをあまりよくわかっていませんでした。
とりあえずnoImplicitAnyを無効にしたらコンパイルが通る、コンパイルエラーが消えるということで、noImplicitAnyをオフにした状態でコンパイルを通してそこからプロジェクトを始めてしまいました。
1年数ヶ月かけてnoImplicitAnyのエラーをほぼゼロに
そして、ここからがこの話の本題です。我々は、このようにnoImplicitAnyを無効にしたまま開発を進めてしまいました。そして数万行のnoImplicitAnyが有効な状態だとコンパイルエラーが出てしまうようなTypeScriptコードが、我々の手元に残りました。それに対して我々は、あとから徐々にnoImplicitAnyを有効化するという施策を取りました。
ではいきなりですが、その結果をお見せします。今出ているグラフは我々のプロジェクト、LINE証券のフロントエンドのTypeScriptのコードの行数とそれに対する暗黙的なnoImplicitAny有効時のコンパイルエラーの数が出ています。青く表示されている右肩上がりの線がTypeScriptの行数で、緑色の真ん中が山になっている線がnoImplicitAny有効時のコンパイルエラーの数です。
我々は、実際にはnoImplicitAnyを無効にしていたので、これらのコンパイルエラーは出ていなかったんですが、もし開発のそれぞれのタイミングでnoImplicitAnyを有効にしていたら、どれくらいのコンパイルエラーが発生していたかという潜在的なnoImplicitAnyエラーの数を表す指標になっています。
これがゼロであれば、晴れてnoImplicitAnyを有効にできるコードベースであることになります。このグラフは、まず左半分に注目していただくと、主にサービスのリリースまでの時期ですね。TypeScriptのソースコードの量はこのように順調に増えて数万行に達しています。
それに比例するように、noImplicitAnyのエラー数も増加しています。このことから、我々の経験として、noImplicitAnyが無効の状況ではそれを放置すると自然に型安全性が低下してしまう。つまり潜在的なnoImplicitAnyのエラー、勝手にanyが発生してしまうような危ないコードが増えてしまうことがわかります。
ピークの時点ではnoImplicitAnyのエラー数は550程度まで達していました。この時点から我々はnoImplicitAnyの無効化の施策を開始しました。その施策の効果が現れているのはグラフの右半分です。
1年数ヶ月かけて我々はnoImplicitAnyのエラーをほぼゼロまでもっていくことに成功しました。ついさっき測ってみると、我々のコードベースに残っている残りのnoImplicitAnyエラーは11個でした。
このようにnoImplicitAnyのエラーは一転して減少に転じたのですが、その間も我々のコードベースは成長を続けています。noImplicitAnyを無効化するために成長が止まってしまったというようなことはありませんでした。
diffがあったプログラムに対してだけエラーをチェックする
またこの結果をよく見ていただくと、最初いきなりnoImplicitAnyエラー数が半減している場所があります。これは我々がnoImplicitAnyに対して短期決戦を挑んだ場所です。
たまたまこの時期に1週間くらい、やや暇な時間がありましたので、TypeScriptのエラーをみんなでTypeScriptのnoImplicitAnyのエラーを解消してみた。もちろんこれはあまりQAに影響のない範囲で、ということになります。
しかしやってみたのですが、我々はnoImplicitAnyのエラーを半分くらいにしたところで力尽きてしまって、一気に解消するのは無理で、長期的な施策が必要であると認識しました。それからは、長期的な戦略に切り替えました。
我々がやったことは、CIでdiffがあったTypeScriptプログラムに対してだけTypeScriptのnoImplicitAnyのエラーをチェックすることです。diffがあったファイルに対してnoImplicitAnyのエラーが発生していたら、それが弾かれてCIが落ちることになります。
ですので、我々はそのエラーを修正する必要があります。このようにプログラムの全体に対して一律にチェックするのではなく、部分的にチェックを行うのは、今だと例えばreviewdogみたいな製品がやってくれますが、我々はこれを自前で実装しています。
こうすると、新しいnoImplicitAnyのエラーは発生しなくなります。なぜなら新しく書かれたコードは必ずCIによってチェックされ、noImplicitAnyのエラーがあると弾かれるからです。こうすることで我々は放っておくと、noImplicitAnyが増え続ける問題に対処しました。
また、すでに存在しているnoImplicitAnyのエラー、既存のファイルに関しても変更があったタイミングでそのファイルはdiffがあったファイルとして検出され、CIでnoImplicitAnyのエラーがチェックされます。ですから既存のファイルについても変更があったタイミングで徐々にnoImplicitAnyのエラーが直されていきました。
noImplicitAnyのコンパイラオプションを徐々に有効化することによりクオリティを上げた
この結果として、もちろんnoImplicitAnyのコンパイルエラーを減らしましたのでany型が自然に発生してしまうことを抑制し、我々のコードはより型安全なものになりました。これが我々の施策の目的であり、主なメリットだったのですが、それに付随するよかった点として、メンバーのTypeScriptスキルが大きく成長しました。
最初は新しくCIに追加されたnoImplicitAnyのチェックのせいでCIがしょっちゅう落ちていたのですが、最近ではnoImplicitAnyのエラーによってCIが落ちることはめったになくなりました。我々は最初からnoImplicitAnyに適合するようなTypeScriptコードを書けるようになったということです。
また徐々にnoImplicitAnyのエラーを減らしていくという戦略を取った結果、最初に問題として申し上げたnoImplicitAnyを直すためのまとまった工数が取れないといった問題や、開発スケジュールに影響を与えてしまう、あるいはQAの負担が大きい問題を克服できました。
特に変更されたファイルに対してのみnoImplicitAnyのエラーを直す戦略では、noImplicitAnyのためだけのQAコストはまったく発生しません。これによってQAコストが特に大きいという問題にも対処しました。
この戦略はぜひおすすめしたいのですが、少し注意点があります。レビューは慎重にやってくださいということです。これは一番ひどい例ですが、noImplicitAnyのエラーはこのように引数の型に明示的にanyと書いてしまってもエラーを消してしまうことができます。
しかしこのようなことをしたら型安全性の観点ではなんの意味もありません。noImplicitAnyのエラーのいいところは、any型が発生してしまうのを消せる点にありましたから、any型をわざわざ自分で発生してしまうのは非常に本末転倒なことです。
レビューを行う際には、その型定義がよい型定義となっている、よい型定義によってnoImplicitAnyのエラーが消されていることをチェックする必要があります。我々はTypeScriptのコードを書いているわけですから、TypeScriptコードの型定義がいいかどうかをレビューでチェックするのはある種当然のことではないかと思います。
上流の型定義は最高のクオリティを求める
もう1つの注意点として、短期決戦、つまり一気にガクッとnoImplicitAnyのエラーを解消することは最初にやったほうがよいです。これをやると残りの作業が楽になります。特に一番依存性の上流にある、ほかからよく参照されるようなコードは最初に直すといいでしょう。
また上流にあるモジュールほど、その型定義には全力を尽くすべきです。上流の型定義のクオリティが悪いと、下流の型定義にも悪い影響を及ぼしてしまいます。ですから、上流の型定義は最高のクオリティを求めてください。チームにTypeScriptマスターがいれば、ぜひその人にやってもらってください。
ということでまとめに入りますが、このセッションでは我々の型安全性に対する取り組みを3つ紹介いたしました。このさまざまな方法によって我々は型安全性をさらに高めて、さらに開発者の体験も良くなっていると考えています。みなさんの参考にしてください。
具体的にこのセッションでは3つの話題を紹介いたしました。まずXLTの話題では、コード生成によって最大限の型安全性、そして開発者体験を得られたという話をしました。次にtypescript-eslintの話題では、TypeScriptコンパイラ本体によっては検出されないさらなる安全性を求めてtypescript-eslintルールを作った話をいたしました。
そして最後に3つ目の話題として、noImplicitAnyのコンパイラオプションを徐々に有効化することによってコードのクオリティを上げた話をしました。この3つの経験をみなさまもぜひ活かしてほしいと思います。
このセッションは以上となります。ご清聴ありがとうございました。