某ECサイトのプロジェクトで、Reactをフロントエンドのスタックとして採用しています。
TypeScriptは約3年前に導入していましたが、その時に型エラーの解決は行なっていませんでした。
その理由としては、型エラーの数が多く解決に時間がかかる、他タスクも進行中のものが多くコンフリクトが大量に発生する可能性がある、と非常にコストが高かったためです。
そこから時間は経ちましたが、作業に余裕が出てきたのでチーム内で話し合い、全ての型エラーを解決していくことになりました。
本記事では、解決に踏み切った経緯やその際の方針、苦労した点などについてお話しします。
型エラーの検出方法
型エラーの検出には、tsc
コマンドを利用しました。
型チェックのみを実行する場合は--noEmit
オプションを指定します。
tsc --noEmit
tsconfig.json
がある場合は、そちらに設定されているオプションで実行されます。
tsconfig.json
{
"compilerOptions": {
"noEmit": true,
..
}
..
}
本プロジェクトでは、約6,000件の型エラーが検出されました。
型解決不要なファイルなどもあったため、最終的には延べ約3,000件でした。
プロジェクトの規模としては、コード量が約22万行です。
型エラーの解決に踏み切った経緯
解決に踏み切った経緯としては、大きく2点あります。
- 型チェックにより保守性を高める
- コード補完を用いて開発スピードを高める
型エラーが原因のエラーは元々多くはなかったですが、型チェックを有効にすることで開発段階でエラーに気づくことができるので、より保守性が高まると考えました。
コード補完については、型エラーの影響で補完されないところがあり、開発スピードが著しく低下する場面がありました。また、リファクタリングの際にも予期せぬエラーが起こったりと影響範囲の特定が難しい場面がありました。
上記を考慮して、コストよりメリットの方が大きいと判断し、型エラーの解決に取り組みました。
型解決していく上での方針
エラー件数が多かったこともあり、作業に取り掛かる前にいくつか方針を決めました。
- 作業者の負担になりすぎないようにする
- 型解決しやすいところから始める
- 基本的に動作は変えず、型解決のみに焦点を当てる
他タスクと並行して作業する場合もあるので、1週間単位で最低限のノルマを決めつつも、日々の進捗管理は個人にお任せしました。全体の進捗は型エラーをリスト化していたので、そちらを元に把握しました。また、予期せぬ不具合を防ぐため、型解決のみに焦点を当てるようにしました。
実装面でもいくつか方針を立てていたので、特筆して重要な項目を2点ご紹介します。
- 型アサーション(as)は極力使用しない
型アサーションを使用するとコンパイラでエラーを検知できないため、型解決が困難なところ以外は使用を避けました。使用する場合はその経緯をコメントで残しました。
// 悪い
type Foo = { hoge: string; };
const foo = {} as Foo; // no error.
// 良い
type Foo = { hoge: string; };
const foo: Foo = {}; // Property 'hoge' is missing in type '{}' but required in type 'Foo'.
- 同じ型を重複して定義しないようにする
React.ComponentProps<T>
Pick<T>
Omit<T>
などを使用して型定義の重複を防ぐようにしました。仮に参照元の型が変更されたとしても、参照先では修正する必要がないので保守性を高めます。
// 悪い
type FooProps = { foo: string; };
const Foo = (props: FooProps) => <></>;
type Props = { foo: string; };
// 良い
type FooProps = { foo: string; };
const Foo = (props: FooProps) => <></>;
type Props = { foo: ComponentProps<typeof Foo>['foo']; };
or
type Props = ComponentProps<typeof Foo>;
苦労したこと
今回の型エラー解決を通して苦労した点としては、
- 型エラーを1つ直すと芋づる式に型エラーとなるところがあった
型は紐づいているところが多いので、新たに型エラーが生まれてしまう場合がありました。
そうなると、当初の型エラー件数よりも数が増えてしまい、想定より型解決に時間がかかりました。 - 他タスクの影響で新たに型エラーが生まれてしまっていた
既存の型エラーの影響で型チェックを有効化することができなかったため、他タスクの影響で新たに型エラーが生まれていました。開発時に気をつけていても型エラーが増えてしまうことは想定できたので、スケジュールを決める際に考慮するべきでした。 - まとめて(画面毎)リリースしようとした際に、コンフリクトが目立った
当初はある程度まとめてリリースする予定で進行していました。まとめてしまうと、他タスクとの兼ね合いや多くの画面にまたがる型エラーの修正などで、リリース時のコンフリクトが多く解決に苦労しました。
実装面は勿論ですが、進行面で苦労したこともありました。
苦労したことを元に、改めて方針を立てるとすれば下記の2点を追加するのが良いと思います。
- 他タスクの影響で型エラーが増えることを考慮したスケジュールにする
- 画面毎など大きいリリースではなく、小まめにリリースしてコンフリクトを防ぐ
まとめ
TypeScriptの型エラーを解決したことで、以下の改善を行うことができました。
- 開発段階で型エラーに気づけるようになり、保守性が上がった
- コード補完が効くようになり改善された
- 仕様面などで問題がありそうなコードを見直すことができた
当初考えていた内容はもちろんですが、コードを確認していると仕様面の問題に気づくこともあり見直すことできました(例えば、APIで受け取っている値と型定義されている内容が違うなど)。
コストは高かったですが、想定以上にメリットがあったので良いリファクタリングとなりました。