機能開発を止めずに、6万行の TypeScript 移行を完了させた開発プロセス

Shingo Sasaki
スタディスト Tech Blog
11 min readSep 24, 2021

スタディスト 開発部 技術支援ユニットの笹木 (@s_sasaki_0529) です。

2021年上半期、およそ6万行の JavaScript コードを TypeScript に置き換える作業を、半年間単独で行いました。

本記事では、機能開発自体を止めずに、どのように走り切ることができたのか、ふりかえりたいと思います。

なお、本記事の内容は、移行開始直後の登壇資料 “大規模 Vue アプリケーションの TypeScript 移行” と、移行完了後の登壇資料 “6万行の TypeScript 移行とその後” と重複する内容を含んでいます。

Teachme Biz と TypeScript

弊社が開発している、マニュアル作成・共有システム Teachme Biz は、iOS/Android や Windows など、マルチプラットフォームで提供されています。

その中でも、作成・管理に多く使われている Webブラウザ版は、 JavaScript と Vue.js を主とした、以下のような技術構成となっており、歴史的経緯もあって TypeScript は使用していませんでした。

移行開始以前の技術構成

Web開発における TypeScript の重要性は語るまでもありません。弊社でも、型がないことによる以下の四大リスクを抱え、不具合が発生しやすい上、開発効率が低い状態が続いていました。

四大リスク
  • 危険: Null を含むどんな値が入っているか推測しづらい
  • 無秩序: コードの自由度が高まり、一貫性のないコードが量産される
  • 非効率: エディタの推論が満足に効かず、タイポが起こりやすい
  • 暗黙知: 設計に関する情報がコードから得づらい

上記の四大リスクは、型の力で以下のように解決することができます。

かた の ちから ってすげー

これまでは四大リスクをだましだまし受け入れ続けてきましたが、長期的なスケールや、Vue 3 へのマイグレーションなどを見越すと、移行は今しかないという判断に至りました。

移行範囲

移行を決意したとはいえ、どこまでをどのレベルで移行するのかを予め決める必要があります。

結論から言うと、以下のような方針で移行を行うことにしました。

  • .js ファイルは(一部の例外を除いて) 全て移行する
  • .vue ファイルは一切移行しない
  • strict: true のルールを設定する
  • plugin:@typescript-eslint/recommended を使用する

Vue を対象外としたのは、Vue 3 へのマイグレーションを考慮した時、どのみちコンポーネントの書き直しが発生したり、型の使い方が変わったりと二度手間になりえるからです。

.js ファイルについては、基本的に全てのファイルを対象とし、 tsconfig や eslint での厳しい設定を付与します。これによって、implicit any や ts-ignore を使用した、”移行はしてるが全然型安全じゃない”という半端な状態を減らします。 (明示的な any 自体は禁止せず、今後の課題とする)

開発チームとの認識合わせ

既存コードの TypeScript 移行を進めても、各開発チームが新規コードを従来どおり JavaScript で書いてしまうと、アキレスと亀のような状態になり、移行がいつまでも完了しません。

そこで、まずは開発チーム全体に、本記事で書かれているような背景とプロセスを共有し、新規コードは基本的に TypeScript で書いてもらいつつ、既存コードを改修する場合も、可能な限り TypeScript を使うように認識を揃えました。

もちろん多くのメンバーは TypeScript 未経験であるため、相互レビューを強化したり、実践 TypeScript の輪読を行うなど、社内への TypeScript の布教を心がけました。

輪読に使用した書籍

移行プロセス

移行対象が .js ファイルのみとはいえ、コードベース上のJSファイルは、テストコードも含めると累計で6万行存在するため、計画的に移行を進める必要があります。

充分な時間があるとはいえ、他の開発チームの業務は止めず、なるべく邪魔にならないようにするためには、移行するファイルと機能開発で修正されるファイルの衝突を、最小限に留める必要があります。

そのため、移行用の作業ブランチを切るということはせず、ファイルまたはフォルダ単位で小さく移行し、個別に PR を作成、素早くマージ(≒リリース) するというサイクルを繰り返しました。

全て master ブランチ向きの PR

なお、細かい移行順序や工夫点は、UIT INSIDE で話されていた LINE 様の事例を参考にしています。 (いつも楽しく聴いてます!)

検証プロセス

前述のように、小さく移行してリリースのサイクルを回すということは、それだけ本番環境のコードが頻繁に書き換わるということです。

開発のリスク軽減のための TypeScript 移行のせいで、新たに障害が起こるようでは本末転倒。ここでバグを出すわけにはいきません。

幸いにも Teachme Biz の開発環境は、 CI/CD プロセスが充分に整備されています。

コードをプッシュするたびに、以下のテストが CircleCI 上で実行されるので、基本的には CI が通れば OK と判断できます。

  • 単体テスト(Jest)
  • E2Eテスト(Selenium)
  • Visual Regression Testing (Storybook + reg-suit)
内製のE2Eテストレポート画面

自動テストをパスしても不安の残る改修がある場合、検証環境を立上げて、個別に手動検証を行います。

検証環境については、実行基盤が kubernetes に移行したこともあり、簡単なコマンドで、インスタントな独立した環境を立ち上げることができます。kubernetes については以下記事を御覧ください。

ここまでしてなお不安が残る場合、別途 QA グループに相談します。開発者自身では見つけられない、思いつきもしない不具合を、的確な検証で探り出してもらえます。

リファクタリング

TypeScript 移行に関しては、多くのブログや書籍で事例が紹介されていますが、リファクタリングを同時に行うべきか否かという問題がよく上がります。多くの場合、リファクタリングは別枠考えるべきとも言われています。

以下の事例を具体的に見てみましょう。

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

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

全面的に同意です。多くの場合はその選択が正しいと思います。

しかし弊社では、逆にリファクタリングをイケるところまでやりながら TypeScript 移行しようという方針にしました。主な理由は以下のとおりです。

  • 前述の検証プロセスが非常に充実しているので、デグレリスクが低い
  • 自身が専任で TypeScript 移行を行っているため、時間が充分にある
  • 未来は予想できないため、「あとでリファクタする」で本当にリファクタするとは限らない
  • リファクタせずに現行コードに型を付けると、カオスな型が出来上がる

個人的には最後のが大きいです。型のない世界で、勢いで書かれたコードは、データ構造を無視しがちで、その全てに対応する型を定義しようとすると、引きずられておかしな型が出来上がります。また、このタイミングで旧機能の不要コードや、参照されないデッドコードを洗い出すことで、余計な型定義を減らすこともできます。

そのため、あるべき型を定義し、そぐわないコードを修正するリファクタ作業を同時に行うことで、コード品質自体を高めるというのを目指しました。

進捗管理

全ての JS ファイルを移行対象にするといいつつ、様々な経緯で移行不可能なファイルも少なくありません。そこで、半期の間に 70% の移行を完了させるという OKR を掲げ、それを TypeScript 移行完了のゴールと定義しました。

進捗率のトラッキングは、masterブランチの各コミット時点での TS ファイルと JS ファイルの総数(及び行数) の比率を計測するようにしました。

計測用コードのイメージ

上記スクリプトを用いて集計し、ペース配分を見ながら、半年間移行作業を続けることで、8月末にちょうど目標に到達することができました。

7月後半に急激に増加してるのは Storybook コードの一括置換によるもの

まとめ

本記事では、JavaScript のコード6万行を、機能開発を止めずに、1人で TypeScript に移行することができた要因を整理しました。

弊社では以下のような条件が揃っていたことから、TypeScript 移行をリファクタリング込みで実現できたと言えます。

  • 技術支援ユニットという、機能開発から独立した部隊が存在する
  • 各社の事例を事前に分析し、進め方を決めてから着手した
  • 期間と範囲を決め、トラッキングしながら集中対応した
  • CI/CD によって、自動テスト/手動テストのための仕組みが揃っていた

最後に

スタディストには、こういった開発基盤の整備に全力を注げる環境が用意されています。今後さらなる事業拡大を目指すためにも、より多くの仲間を求めているので、ご興味がありましたら、ぜひ一緒に働きましょう!

--

--

Published in スタディスト Tech Blog

スタディストがお届けする発展と成長の物語。技術、デザイン、組織、マネジメントなどを発信します。

Written by Shingo Sasaki

Webエンジニアです。

No responses yet