LINE証券フロントエンドにおける型安全性への取り組み

鈴木僚太氏(以下、鈴木):このセッションでは『LINE証券フロントエンドにおける型安全性への取り組み』についてお話いたします。私はフィナンシャル開発センターの鈴木僚太と申します。よろしくお願いします。

最初少し自己紹介をさせてください。私は2019年4月に新卒でLINEに入社しました。それ以来、現在までLINE証券というサービスのフロントエンドを担当しています。インターネット上では@uhyo_という名前で活動しているので、それで私のことをご存知の方もいるかもしれません。

私はTypeScriptというプログラミング言語が大好きで、これまで個人のブログ、あるいはQiitaという技術記事投稿サービスでTypeScriptに関する記事を投稿したりしていました。

また最近は、趣味でTypeScriptへのコントリビューションも行っています。またこれは少し宣伝なのですが、現在TypeScriptの入門書を執筆しています。来年に発売できるかと思いますので、みなさん楽しみにお待ちいただければと思います。

次にLINE証券についても軽く紹介します。LINE証券は、名前のとおりLINEアプリ上で証券取引ができるサービスで、株やETF、投資信託といったものを取引できます。LINE証券は去年(2019年)の8月にリリースして以来、現在までハイペースな機能の追加が行われています。

LINE証券の開発における3つのMUST

ではここから、LINE証券の開発についてお話いたします。LINE証券のフロントエンドの技術スタックは、TypeScriptとReactを使用しています。個人的にはTypeScriptとReactという組み合わせが、現在のフロントエンドの王道なのではないかと感じています。

LINE証券のフロントエンドを担当するチームは、10人以上のフロントエンドエンジニアがおり、LINEの中でも比較的大規模です。コード量としては、LINE証券はTypeScriptが15万行と、あともう少しという量になっています。

そんな我々のLINE証券の開発について考えてみると、3つのMUSTがあります。最初に、我々はサービスを落とすことなく提供し続けなければならないと考えています。特にLINE証券は金融サービスなので、なおさらバグあるいは障害のせいで止まってしまうことが決してあってはいけません。

ですから、我々はバグで株の取引ができないことがないように、非常に気をつけていますし、さまざまなエラーハンドリングも非常に気をつけながら行っています。

次に我々には、開発の速度も求められています。先ほど申し上げたとおり、LINE証券は去年の8月にリリースしてまだ1年と数ヶ月という、育ち盛りのサービスですので、次から次へと新しい商品や、あるいは案件が降ってきます。我々はLINE証券のサービス提供のために、それらに迅速に対応しないといけません。

最後に我々は、腐りにくいコード、長く保たれるコードを書かないといけません。そのために、よい設計のコードは非常に重要です。育ち盛りのサービスですから、コードが古くなったからといって書き直す時間はあまり取れず、それよりも新しい商品の開発に力を入れなければいけません。

型があると設計の精度は向上する

このような我々がもっている3つのMUSTに対して、型安全性は非常に有効です。まず型があると、当たり前ですがバグを減らせます。我々が普通、型安全性というときは、このバグが減らせることを指していまして、これが型、あるいは型システムの非常に基本的な効用であると言えます。

それに付随する要素として、型は開発の速度を向上します。代表的な例がエディタによる入力補完ですね。最近のエディタでは、プログラムの一部だけを入力することで型情報を用いてプログラム全体の入力を支援してくれる、そういった入力補完の機能がついています。

また入力補完だけではなく、型はドキュメントとしての役割も果たします。これによってプログラムの読解する速度が向上するといった効果があり、開発の速度の向上の手助けとなります。

また私の考え方ですが、型は設計の基礎になると考えています。私がよくやるのは、まず型のレベルでプログラムの設計を考えて、その型に合わせるように、あるいはそれに辻褄を合わせるように実装を書くことを非常によくやっています。

こういった観点から、型があると設計の精度が向上して、それに伴ってプログラムのコードの品質も上がると我々は考えています。

このセッションでは、我々が型安全性を最大限獲得するために行っている3つの取り組みについて紹介いたします。1つ目のテーマは、XLTという社内のインターナショナリゼーションツールについてお話いたします。我々は、最大限の型安全性のためにXLTによるコード生成を行いました。これに関するお話をいたします。

2つ目の話題として、TypeScriptではチェックされないようなバグを防ぐために、我々独自のTypeScript-eslintルールを実装した話。そして3つ目、これが本日のメインディッシュで、我々が1年かけて徐々にコードベースのnoImplicitAnyというコンパイラオプションを有効化に近づけていった話をいたします。

XLTとは何か?

ではまず、XLTのお話からしていきたいと思います。XLTは先ほどちょっと申し上げたように、社内向けのインターナショナリゼーションシステムです。XLTはCross-Language Translation Toolの略称だそうです。

XLTのシステム自体は、テキストを登録する管理システムと、そのテキストをエクスポートできるAPIからなっています。主に企画の方々がXLTのシステムにデータを登録して、我々はXLTのシステムからAPIを通じてエクスポートしたテキストをアプリ内で使っています。

XLTのデータは、今スライドの左側に出ているようなキーバリュー形式のデータとなっています。キーを見ますと、このようにドットでキーの階層が区切られているのが特徴的です。我々はXLTのシステムからこのようなデータをダウンロードし、ちょっと加工して、これは後ほどお話ししますが、我々のコードベースにコミットします。

テキストに関しては、コミットをせずにサービスをビルドするときにXLTのシステムからデータを引くとか、あるいはランタイムでXLTからCMSのようにデータを引くという選択肢もありましたが、我々はコードの一部としてXLTからエクスポートしたデータをコミットする選択を取っています。

その理由は、我々がリリースする文言がどのようになっているかを、コミットの単位で我々の管理下に置くためですね。

ちなみに、先ほどからインターナショナリゼーションと申し上げていますが、残念なことに現在のLINE証券のサポート言語は日本語だけとなっています。それにもかかわらずXLTを導入した経緯には、ソースコードの中身とそのアプリに現れる細々としたテキストを分けて管理したいという動機がありました。

完全に型安全になるように作ったXLT関数

さて、このXLTが我々のアプリケーションのコード上でどのように使われているかなんですが、今スライドの左側に映っているように使います。一番上の行は最も単純なXLTの使い方です。XLTという関数があらかじめ用意されていて、そのXLT関数にキーを渡すことで、文字列vが選べます。

我々のXLTのAPIの特徴として、パーシャルアプリケーションをサポートしていることがあげられます。2つ目のXLT呼び出しではtを得ていますが、このときXLT関数にはキーの全体ではなく、そのうち一部だけを渡しています。こうすると、返り値は文字列ではなく関数となります。その関数に残りのキーを渡すことで、文字列が得られる仕組みになっています。

このようなAPIにすると、複数のキーに共通するプレフィックスを省略できて、うれしくなっています。特に、同じ画面では同じようなプレフィックスが使われるようにXLTの構造がなっていますので、それに伴ってコードの量を省略できます。

我々は最初に申し上げたように、型安全性に非常に気を遣っています。ですので、このXLT関数も、完全に型安全になるように作りました。例えば、存在しないキーをXLT関数に渡したりするとコンパイルエラーになります。これは、TypeScriptの優れた型システムの機能、具体的には文字列のリテラル型、あとユニオン型といったものによって実現されています。

当然ながら、オートコンプリーション、つまり入力補完も非常に快適です。今画面に映っているのはXLTにキーの一部を渡して得られた関数ですが、今動画でお見せするように、このtに対してこのように入力補完が働きます。これによって、我々は入力ミスをすることもないし、キーをいちいち覚えたりする必要もない。入力補完を頼りに、快適にXLTを用いた開発ができます。

XLTの型安全性を実現するためのコード生成

では次に、以上のことをどのように実現しているのかという話をします。我々は今のようなXLTの型安全性を実現するためにコードジェネレーション、つまりコードの生成を行っています。画面左に映っているのはコード生成によって作られた型定義です。このようにコード生成では、存在するすべてのキーを網羅した巨大な型を作成します。

この型では、各キーにそのキーで得られるものが割り当てられていて、完全なキーの場合はstring型、あるいはパーシャルなキーの場合は、その残りの部分の文字列の集合を表すインターフェースになっています。

ちなみに生成される型は、このようにすべてのキーに対して型定義が生成されるので、非常に大きいのですが、先ほどお見せしたXLT関数のランタイムのコードはどのキーでも共通になっていますので、XLTデータによらない固定の小さなコードとなっています。

もちろん、テキストの生データもバンドルする必要があります。この目的のために、我々はパーシャルなキーをそのままJSONなどの方法でもつのではなく、ドット区切りのキーをネストしたオブジェクトに変換しています。

これ、パーシャルなデータを扱う関係で、同じデータが何回も型定義に現れたりするんですが、もうすぐリリースが期待されているTypeScript4.1のテンプレートリテラルタイプで解決できるのではないかと、個人的に期待しています。

これは余談になりますが、XLTのデータがシステム上で更新されたら、我々はそれをコードベースに反映する必要がありました。実はこれは、botが自動的に行ってくれるようになっています。Slackにいるbotに、このようにXLT updateと指示を出すと、GitHub上でbotがプルリクエストを作ってくれるようになっています。

もう1つ余談を話しますと、先ほどXLTに文字列を渡して、その文字列に対応するキーをレンダリングする仕組みであると言いました。これを単純に実装すると、XLT関数にすべてのテキストデータが紐付くことになりまして、XLTを使った時点ですべての画面のテキストデータを読み込む必要が発生してしまいます。

これはパフォーマンスの観点から望ましくないので、我々はコードスプリッティングをしたいと思いました。これをある程度実現する仕組みとして、我々はBabelによるコード変換を実装しています。

テキストデータを、あらかじめトップレベルのキーごとに別々のJSONファイルに分割して、XLT関数の呼び出しをBabelが見たら、そのデータだけをインポートして使うようにコードを変換しています。

この話は一見型と無関係に見えますが、ちゃんとXLT関数に対する型チェックが通っていることを前提にこのような変換を行っていますので、これも型安全性が一役買っていることになります。以上が、XLTのお話でした。

自前のTypeScript-eslintルールを作った話

次は我々が自前のTypeScript-eslintルールを作った話です。我々はReactでアプリを作っていますので、このようにJSXを書きます。JSXでよく出てくるパターンは今ソースコード上に、画面に出ていますように条件 && JSXというかたちで書かれるコードです。

これは&&の左に書かれた条件が満たされるときのみその要素を表示する。条件が満たされないときはなにも表示しないコードになっています。

これが実現されるのは、Reactがfalseをなにも描画しない機能によるものです。「条件 ? コンポーネント : null」というように、条件演算子を使う方法もあるんですが、&&を使ったほうがシンプルなので、我々はこの書き方をけっこう使っています。

ところがこの書き方が原因で、バグが発生してしまったことがありました。今出ているコードが、実際にバグが発生したコードです。コードの真ん中あたりに見えるnoticeTotalという引数は、実はnumberまたはundefined型になっています。

このコードの意図は、まずnoticeTotal && でnoticeTotalがundefinedの可能性を除外しています。それに対して、次にnoticeTotalが1よりも大きいという条件をかけて、そのどちらも満たされた場合にのみ、spanを表示するようになっています。

noticeTotalはnumberまたはundefined型なので、最初のnoticeTotal &&がないと次のnoticeTotal > 1という部分がコンパイルエラーになってしまいます。しかしここで問題があって、もしこのnoticeTotalという変数に0が入っていたらどうなるのか、これが問題でした。

実際の表示はこのようになりました。これがnoticeTotalが0のときの表示です。何がおかしいかわかりますか?そう、実はここに余計な0が出てしまっていますね。これがこのコードに潜む問題です。

今回発生したバグをまとめ直すと、このようになります。まずnoticeTotalが0だったので、「noticeTotal &&」という式が0に評価されました。そしてReactは、falseはなにも描画しないんですが、0と来たら0というように描画します。まあ、そうしないと困るのでこれは仕方ないですね。

なので、問題は条件としてtrue、falseという真偽値ではなく数値を書いてしまったことにあります。if文とか条件演算子では条件に数値が来ても真偽値に変換されるので問題ないのですが。&&の場合はそういうことがなくて、&&の返り値まで数値が伝播してしまったことによってこのような問題が発生しました。

我々はこのかたちを、つまり&&の左にnumberが来るようなかたちを機械的に禁止したいとに思いました。一番簡単なのはそもそも条件 && 要素というパターンを全部禁止することですが、それはあまり賢くないなと思い、我々はもう少し賢いやり方を取りました。

TypeScript-eslintルールとは何か

それがtypescript-eslintルールを作ることによるソリューションです。まず、少しTypeScript-eslintルールとは何かのおさらいをしようと思います。

現在JavaScriptに対するlinterのデファクトスタンダードになっているのがeslintです。そして、eslintをTypeScriptプログラムに対して使えるようにするのが、typescript-eslintです。

typescript-eslintは、型を見て判断するlintルールが書けるのが特徴ですので、JSXの中で&&の左がnumber型だった場合にエラーにするというtypescript-eslintルールを作りました。

この話では、typescript-eslintルールの作り方にも少しだけ触れておこうと思います。eslintでTypeScriptコードをチェックするときは、カスタムパーサーを通じて、裏でTypeScriptコンパイラが走っています。

typescript-eslintのルール自体は、普通のeslintルールと同じように作ることができるのですが、ルール内でTypeScriptのコンパイラに由来するTypeCheckerインスタンスを取得できます。画面左のコードは、我々の作ったtypescript-eslintルールの一部なのですが、このTypeCheckerインスタンスを取得する部分となっています。

次はチェックしたい式のかたちをこのようなクエリで指定します。これはtypescript-eslint特有ではなくeslint全体に使える機能ですね。

このクエリではJSXエレメントの中のJSXexpressionContainer……中括弧のやつですね。その中のLogicalExpressionにクエリを指定しています。それが&&というLogicalExpressionだった場合は、&&の左の式の型をTypeCheckerを通じて取得して、あとは左の式の型がnumberだったら弾くだけになっています。

一見、typescript-eslintルールを作るのはハードルが高いと思われるかもしれませんが、実はそんなに難しくはありません。もし型を見て禁止したいプログラムの書き方があった場合には、typescript-eslintルールを作る選択肢をぜひ試してみてください。

残念ながらこのtypescript-eslintルールは今OSSとして公開していません。個人的にはもう少しtypescript-eslintルールの野良のものが増えたらいいなと思っています。