heyのSTORESでECの開発をしている@nkobaです。 この記事ではフロントエンドでTypeScriptを導入した話について発信していきます!
TypeScriptとは
TypeScriptはJavaScriptにリッチな型システムと静的な型検査を付加したプログラミング言語です。
TypeScriptが利用される背景としては、JavaScriptがとても柔軟なプログラミング言語であることがあります。 JavaScriptはプロパティ名を間違っていたり、意図しない値が入っても実行時までエラーになりません。そのため、エラーの調査をすることが難しい言語です。
一方、TypeScriptは静的な型検査により実行前に意図しないコードを検知し、未然にエラーを防ぐことができます。 また、Visual Studio Codeなどのエディタで補完が効きやすいなどの恩恵も受けることができます。
そのため新しいフロントエンドの開発ではTypeScriptが採用されることが多くなってきています。 こうした時代の流れを受け、私達のチームでもTypeScriptの導入を検討することにしました。
TypeScriptを導入する
背景
私達はNuxtJSを2年近く利用しており、長らくJavaScriptを使ってスクリプトを記述していました。
STORES ECのフロントエンドチームでは当初から以下の技術を使用して開発を進めていました。
設計としては表示部分はコンポーネントの責務として扱い、できる限りビジネスロジックはVuexに寄せるという方針をとっていました。
プレーンなJavaScriptでこれらの構成を採用することで起きた課題としては、
- VuexのStateの型を変更したときに、全てのpropリレーしている箇所、テストを全て検査する必要がある
- 新しい社員が入った時にAPIやデータの仕様を調べるのが時間がかかる
- TypeScriptが当たり前になりつつある中で採用面で不利を被る可能性がある
特に最初の課題が一番影響が大きかったです。仕様変更やリファクタリングをするたびに手動でファイルをgrepしながら修正し、手動テストを行うのは大変でした。
2つ目の課題はSwaggerを導入する予定があったり、豊富なドキュメントがチームにあるのでまだ問題は小さかったのですが、開発者体験としては決して良くないものでした。
試験的な導入
このようにチームとしてプレーンなJavaScriptを書き続ける課題はあったのですが、リファクタリング、仕様変更、社員の増加というイベント自体はそこまで頻度は多くありません。
そのため、TypeScript導入のための環境設定はプロジェクト化としては行いませんでした。 チームとして週替わりで運用や改善を行う時間が与えられていたため、その中でできる限りやれたらいいという温度感で始まりました。
フロントエンドチームの改善はこのように、隙間時間を自主的に見つけて自分たちで課題を解決しようというスタンスで進められることが多いです。
Vue.jsとTypeScriptの調査
導入時においてはVue.jsの2系を使用していました。その中でTypeScriptを使うには3つの選択肢がありました。
調査したところ、Class API は Vue 3のRFCに採用されず、Composition APIもProduction環境での使用は推奨しないとのこと(調査時点)でした。
消去法的にOptions APIを使うことになりましたが、現状の書き方に近く、移行の学習コストを抑えられそうでした。
TypeScriptの環境設定
基本的に公式ガイドラインに従って作業すれば、大きく迷うところはありませんでした。
設定でつまづいたのは2点でした。
- Lint周りの設定が新しく入ってきた影響で既存のLintが通らなくなった
- Babelの新規構文を入れている箇所がLintで解析できなくなった
どちらも新規で入れる分には起きない問題でしたが、途中から入れたためにLintがうまくいかない箇所が出てきました。
前者はLintの設定を全て書き出して、一旦現状通りになるように調整しました。 後者は調査しましたが、どうにも対処できなかったため、新規構文を旧構文に戻しました。新規構文はJavaScriptのプライベートフィールドなどで、影響は大きくなかったです。
その他にも詳細は省きますが、TypeScriptビルド時にnodeプロセスが大量のCPUリソースを消費するといった問題(解決済み)もあり、環境設定には3〜4週間はかかりました。
TypeScriptを初運用する
背景
環境設定がうまくいっても、一気にJavaScriptをTypeScriptに書き換えようとはならずに、まずは新規画面ではTypeScriptで書いてみることになりました。 感触が良ければ、後々TypeScriptに置き換える時間を作るという方針です。
もしTypeScriptを導入してみてコストに見合わないなら、次以降はJavaScriptに戻すという選択肢も念頭においての導入でした。
勉強会の開催
当時のフロントエンドチームのメンバーにもTypeScript経験者はいましたが、割合はそう多くはありませんでした。 その中でいきなりTypeScriptを書けるはずもなく、週に2回ほどチームで勉強会が始まりました。
勉強会ではプログラミングTypeScript――スケールするJavaScriptアプリケーション開発を各自読んできて、気になったことを議論するという進め方をしました。内容の確認ではなく、実際に応用するためにはどうすべきかの議論に時間を使いました。
勉強会は別のプロジェクトと同時並行でしたので、なかなか時間を確保するのは大変でしたが、何とかやりきることができました。
型エラーとの戦い
導入した最初のプロジェクトは型をうまく通すことができずに、苦戦しました。 Vue.jsの型定義は複雑であり、コンパイルエラーのメッセージを読んでもなかなか理解するのが難しかったです。
できる限り時間をかけて試行錯誤したり、サンプルコードを探したり、ときにはチームメンバーが集まってモブプログラミングするなどして型を頑張ってつけていきました。
特に辛いのはJestとVue Test Utilsを組み合わせた部分でした。 Jestはプラグインを使ってVue.jsのテンプレートの型を解釈しているのですが、これがNuxtJSの解釈と異なっていたり、あるいは純粋に使うとコンポーネント部分がanyになったりと苦戦しました。
そうでなくともこれまでのテストの書き方ではコンパイルが通らない部分もあり、テストとTypeScriptの兼ね合いが一番苦労したと思います。
これら全てを解決できずにignoreやanyで解決したところもありますが、開発するときに十分に型の恩恵を受けられる程度には型をつけるところまではできました。
正式な導入の決定
プロジェクトは型エラーの調査に時間を使った関係でJavaScriptと比べて「体感1.3~1.4倍くらい(メンバー談)」の工数がかかりました。
ただ、最初のプロジェクトだったので想定の範囲内であり、型チェックによる安全性の向上、コード補完によるコーディング効率の向上、既存ライブラリの内容理解の促進などの効果はやはり大きいという結論になりました。
そのため、今後はTypeScriptを中心にして開発を進めていくことで合意しました。
TypeScriptをより活用する
背景
TypeScriptを推進していくことは決めましたが、現実としてJavaScriptとTypeSriptが入り混じったコードベースで運用しているため進め方で考えることが出てきました。
また、大きめの改修を入れたことで改善すべきポイントも見つかりました。 そうした事例をいくつか紹介します。
Vuexの見直し
これまでデータストアやビジネスロジックはNuxtJSに付随するVuexの機能を中心に書いてきました。 ですが、mapXXXの機能や、asyncDataなどを使ってVuexにアクセスした場合の型推論の難しさが大きな課題となりました。
文字列から動的にオブジェクトにアクセスするやり方はTypeScriptでは厳しいということが徐々にわかってきて、新しくデータストアを使うときはVuex以外の方法を検討しました。
具体的にはVue.observableを使って、データストアを作り、テストしやすいようにいくつか工夫したものを使いました。
Vuexではどうやっても型推論できなかったものがプレーンな仕組みを使うことで解決できたので、今後はVuexを使う設計は最終手段にしたいと思っています。 ただ、Vuexのバージョンが上がってTypeScriptフレンドリーになれば、状況は変わるかもしれません。
型の部分的な導入
フロントエンドチームではエラーハンドリング、バリデーション部分を共通化しています。 その使用範囲は大きく、これまでのフォーム全体が影響範囲になります。 このロジックは複雑でできるだけ早く型が欲しいと言われていたため、真っ先にこの部分だけ型定義を書いて修正しました。
型を書いてもJavaScriptで書いてきたこれまでのコードは恩恵は受けられませんが、新規部分はとても読みやすくなりました。 全てTypeScriptで置き換える工数は当面取れないため、できるところから少しずつ型をつけてということでかなりコツコツ改善を進めていくというやり方が今の方法になります。
Swaggerから型定義を生成
社内ではSwaggerを使ってバックエンドの方とコミュニケーションをとっています。
せっかくAPI定義があるので、OpenAPI Generatorを使ってTypeScriptの型定義を作ることができれば、意思疎通も楽になるのではと考え、Swaggerから型定義を生成することを検討しています。
型定義を自動生成したものを使うと具合が悪いケースが当然出てくるので、それを使いやすいように別の型でラップして呼び出すという形で運用を考えています。
将来的にはGitHub Actionsで自動でAPI定義を監視、PRを出すのが目標です。 具体的なやり方はともかく、型の自動生成を通じて、より安全な開発ができるように考えていこうと思っています。
振り返り
これまで投資を続けてきたこともあり、現在ではTypeScriptはフロントエンド開発の速度、体験を向上させる重要な基盤になりつつあります。 初期費用はかかっていますが、最近の開発ではそれ以上の恩恵を受けています。
最近感じているのはTypeScriptを導入することはJavaScriptに型が付くという以上に開発プロセスであったり、設計の見直しを要求するものであるということです。
Vuexの問題が顕著ですが、JavaScriptでは許容されていた設計がTypeScriptでは難しいこともありますし、またSwaggerのように自動生成の可能性が広がるというところで別の視点が要求されます。
組織によって変わるとは思いますが、個人的にはJavaScript + 型という考え方ではなく、TypeScriptファーストで開発を考えることが今のチームにとって良いのではと考えています。 そのため、静的な型検査を持つ言語の開発事例を調べてみるつもりです。
一方で、既存のJavaScriptもまだコードベースに残っているため、その中でどう調和するかという課題についても今後本格的に向き合うことになるのかなと思います。
こうした課題に向き合いながら、より働きやすい開発環境作りに一定時間使えるようなチームを目指して努力していくつもりです。