atama plus techblog
🔧

【TypeScript】ライブラリの型を上書きしてバージョンアップデートの手間を減らす

2024/12/05に公開

はじめに

TypeScriptを使用したプロジェクトでライブラリをバージョンアップデートする際、プロパティ名や型定義が変更されることはよくあります。

特に規模が大きいプロジェクトでは、こうした変更が数十、数百箇所に影響を及ぼすため、手作業で修正するのは非効率でエラーが発生しやすくなります。

弊社でも、使用しているUIライブラリの型変更が原因で、ライブラリのバージョンアップデート作業が滞った経験があります。

具体的には以下のような状況でした。

  • 使用ライブラリ
  • アップデート内容
    • ButtonコンポーネントにdisabledisDisabled という2つのプロパティが存在
    • しかしライブラリのバージョンアップに伴い、isDisabled プロパティのみ参照され、disabled プロパティは存在するものの機能しなくなるissue
  • 対処法
    • プロジェクト全体でButtonコンポーネントの disabled 属性を使用している箇所をすべて isDisabled に置き換える

ざっと確認したところ、影響箇所は100箇所以上あり、手作業での修正は不可能ではないものの大変そうでした。

これを効率よく解決するために、型定義の上書き(declaration merging を活用して一括置換と古いプロパティの使用の禁止を行いました。

本記事ではその手法を紹介します。

解決方法の検討

解決方法を紹介する前に解決策の検討過程を紹介します。
以下の3つの方法を検討しました。

1. 人力で置換する

ゴリ押しで手作業で古いプロパティを新しいプロパティに置換する案です。

  • デメリット
    • 置換に時間がかかり面倒
    • 漏れが発生する可能性がある
    • 漏れがないことの確認が大変
    • 古いプロパティの使用を制限していないため、バグが発生する可能性がある

デメリットが多く他の案を探しました。

2. ラッパー(腐敗防止層)を作成する

UIライブラリの直接使用を禁止し、ラップしたコンポーネントを作成して使用する案です。

このラッパー内部で disabledisDisabled への変換処理を行えば、既存コードを大きく変更せずに対応できそうでした。

  • メリット
    • ラッパー内部で変換処理を行うだけで済む。
    • 一度ラッパーを作成すれば、将来的な他の変更も対応しやすい。
  • デメリット
    • 既存のコードをすべてラッパー経由に置き換える作業が面倒(弊社では直接UIライブラリを参照していてラッパーを経由していなかったため)
    • UIライブラリの直接使用を禁止する作業も必要になる。

案としては悪くなかったのですが、以下の観点で見送りました。

  • 弊社ではラッパーを経由していなかったので、まずラッパーへの置き換え作業が必要になる
  • 今回問題となっているButtonコンポーネント以外の他のコンポーネントもラッパーを経由するか迷う。統一性の観点からは置き換えた方が望ましいが置き換える場合は作業がさらに増える。

3. 型の上書きをして古いプロパティの使用を禁止し、新しいプロパティに置き換える

declaration mergingというテクニックを利用して、ライブラリの型宣言を上書きする案です。

このテクニックを用いてライブラリの型定義と矛盾するように 型定義を上書きすると古いプロパティの使用を禁止できます。(具体的なやり方は後ほど紹介します。)

そして禁止されたプロパティを使用している箇所は型エラーとして検出できます。

declaration mergingとは?

TypeScriptでは同じ名前のinterfaceを複数定義すると宣言がmergeされます。

interface User {
  name: string;
};
interface User {
  age: number;
}

// 以下と同義
// interface User {
//   name: string;
//   age: number;
// }

この挙動をdeclaration mergingと言います。

この挙動を利用してプロパティの型定義を取りえない型(neverなど)に上書きすることで使用箇所で型エラーを発生させるという案です。

declaration mergingに関する詳しい説明は以下の記事が分かりやすかったのでご覧ください。

この方法のメリット、デメリット

  • メリット
    • 使用してほしくないプロパティの使用を型レベルで禁止できる。
    • 型エラーを検出し、使われている箇所をリストアップできる。
  • デメリット
    • 禁止後の置換作業が面倒
      • エディタの一括リネーム機能が活用でき、この置換作業の時間はほとんどかかりませんでした!
    • 型安全性を保っていない部分は検出できない。

最終的に、この案3を採用しました。

実際の手順

以下は実際に行った手順です。

1. 型を上書きする(declaration mergingを活用)

Chakra UIのButtonの型定義は ButtonProps という名前で定義されていました。

以下の型定義ファイルを作成して ButtonProps を上書きしました。

@chakra-ui.d.ts
export * from '@chakra-ui/react'; // 必須: Chakra UIの型解決のため何かしらをexport

declare module '@chakra-ui/react' {
  export interface ButtonProps {
    disabled?: never; // 代わりにisDisabledを使用してください (https://github.com/chakra-ui/chakra-ui/issues/7269)
    // `?`でoptionalにしないとdisabledプロパティが必須プロパティになってしまう
  }
}

上記のコードにより、disabled プロパティを使用すると型エラーが発生するようになります。

この状態でnpx tscを実行すると disabled プロパティが使用されている箇所を一覧化できます。

2. リファクタリングツールを使用して一括変更

次に、エディタ(例: Visual Studio Code(VSCode))のリファクタリング機能を使用して、上書きした型定義ファイルのプロパティ名を disabled から isDisabled に一括でリネームします。


VSCodeを使用した例

すると、disabled プロパティを使用していた箇所が一括で isDisabled に置き換わります!これで置換作業ほぼ完了です!

3. 型定義を元に戻す

型定義ファイルを元に戻し、disabled プロパティを許可しない状態に戻します。

これで disabled プロパティの使用が禁止され、ほぼすべて isDisabled プロパティに置き換わった状態になります。

4. 一括置換できていないところを直す

npx tscを実行して型エラーがないか確認します。

変数を経由しているケースなど一括置換できないケースもあり、そこは手で修正します。

例えば以下のようなケースです。

propsを変数を経由して渡しているケース
import { Button } from '@chakra-ui/react';

const SampleButton = () => {
  const buttonProps = { disabled: true } // ここは自動で置換されない

  return <Button {...buttonProps}>クリック</Button>; // コンパイルエラー: プロパティ disabled の型に互換性がありません。
}

このようなケースは手動で置換する場合見落としやすいケースだと思うので、型エラーが出て漏れに気づけるのは非常に良かったです。

5. テストを実行する

型エラーがなくなったことを確認したら、テストを実行して問題がないか確認します。

型安全になっていない部分は自動置換ができず型エラーも出ないので、そのような場所がないか型チェック以外の方法で確認する必要があります。

弊社の場合はplaywrightによるE2Eテストを実行して確認しました。

テストの結果、置換ができていないことによる不具合が発見されました。
例えば以下のようなコードが置換できていませんでした。

型情報がないpropsを渡しているケース
import { Button } from '@chakra-ui/react';

const SampleButton = () => {
  const buttonProps = { disabled: true } as any;

  return <Button {...buttonProps}>クリック</Button>; // コンパイルエラーが発生しない
}

これらを修正して再度テストまで完了したら置き換え作業完了です!

まとめ

declaration mergingを用いてライブラリの型定義を上書きすることで、ライブラリアップデートに必要なプロパティ名の一括置換を効率的に行えました!

この方法を利用することで、TypeScriptを用いたプロジェクトにおけるライブラリアップデート時の作業を減らすことができると思います。

もし他に効率の良い方法があれば、ぜひ教えてください!

P.S.

今回の経験から、UIライブラリのラップ戦略の重要性を改めて感じました。
今回は別の解決策を用いましたが、ラッパーを経由していればラッパーの書き換えだけで済んでいました。
後からラッパーを導入するのは大変なので新規プロジェクトでは、初期段階でラッパーを作成し、変更に強い設計を心がけたいと思います。

atama plus techblog
atama plus techblog

Discussion