ChatGPT に聞きながら React.FC を function に書き換えてみた
はじめに
この記事は GENDA Advent Calendar 2024 5日目の記事です。
株式会社GENDA FE/BEエンジニアの shinnoki です。
GENDA ではテック組織一丸となって生成 AI を用いた開発生産の向上に取り組んでおり、GitHub Copilot や ChatGPT Team が業務で使えます。
私はコードを書くときに GitHub Copilot によるサジェストは活用している[1]のですが、いまいちそれ以上に生成 AI を活用できいない感じがしていて、もっと使っていきたいというな状況です。
そこでいずれやろうと思っていた
const Component: React.FC<Props> = (props) => { ... };
で定義されたコードを
function Component(props: Props) { ... }
に書き換えるというタスクをやってみました。
この書き換え、やることはシンプルなのですがいざやってみると手で書き換えるには絶妙に手数が多く面倒です。
個別のファイルであれば VSCode で GitHub Copilot を使っている場合はインラインの Copilot Chat (Macの場合 ⌘
+i
)で雑に to function
等と指示するといい感じに直してくれます。
Copilot Edits を使うと複数ファイルにわたる編集ができるのですが、2024年12月時点では以下の通りの制約があり、中規模以上のプロジェクトでは一度に書き換えるのは難しそうです。[2]
- The working set is currently limited to 10 files.
- Copilot Edits is limited to 7 editing requests per 10 minutes.
Cursor は使ったことがないのですができそうな気配がしているのでいずれ試してみたいです。
ChatGPT に聞いてみた
というわけで ChatGPT Team (GPT-4o) に聞いてみました。
基本は
プロジェクト内の React.FC を全て function に変更したいです
というような質問ですが、質問のたびに回答にブレがあるため文言を少し変えたり ChatGPT Desktop の VSCode 連携 をオンにしたりして何度か質問してみたところ、おおむね以下の回答が得られました。
- VSCode などのエディタで正規表現で一括置換する方法
- シェルスクリプトを使って正規表現で置換する方法
-
eslint-plugin-react の
react/function-component-definition
ルールを使う方法 - jscodeshift や ts-morph などを使い codemod を実装する方法
eslint を使う方法がハマれば確実ですが、今後別のプロジェクトでも気軽に適用できる方法として、エディタでの一括置換を試してみました。
いくつかの回答の中で、以下の正規表現が一番良さそうでした。[3]
置換対象
次の正規表現を使用します。これにより、React.FC を使った全体の記述を複数行にまたがる場合でも対応できます。
export const (\w+): React\.FC<([\s\S]*?)> = \(([\s\S]*?)\) => {
置換後
置換後のパターンは次のようにします:
export function $1($3: $2) {
説明
[\s\S]*?
: 改行を含むすべての文字列を非貪欲にマッチさせる。(\w+)
: コンポーネント名をキャプチャ。React\.FC<([\s\S]*?)>: React.FC<...>
の型部分をキャプチャ。\(([\s\S]*?)\)
: 関数の引数部分をキャプチャ。{
: 関数の本体を開始する部分をそのまま維持。
実際の修正内容
ChatGPT の回答を元に少しアレンジをして、実際に業務で開発しているプロジェクトに適用してみました。
書き換え前に React.FC
でプロジェクト内検索したところ553件、315ファイルがヒットしました。
const Component: React.FC = () => {
を置換
1. ChatGPT の回答では
const Component: React.FC<Props> = (props) => {
となっている前提でしたが、 props を持たないコンポーネントで
const Component: React.FC = () => {
となっているケースもありました。
正規表現を改良して対応することもできそうですが別々に置換したほうがシンプルなため、まずは簡単なこちらのケースから置換しました。
- 検索ワード:
const (\w+): React\.FC = \(\) => \{
- 置換ワード:
function $1() {
- 対象: 210件、164ファイル
const Component: React.FC<Props> = (props) => {
を置換
2. 次に props を持つケースを置換しました。
通常の正規表現であれば \s
で改行にもマッチするのですが VSCode の場合複数行にわたる検索には \n
を含める必要があった[4]ので修正しました。
- 検索ワード:
const (\w+): React\.FC<([\s\S\n]*?)> = \(([\s\S\n]*?)\) => \{
- 置換ワード:
function $1($3: $2) {
- 対象: 331件、207ファイル
3. 型エラーが出たファイルを修正
これは今回のプロジェクト特有の問題だったのですが
const Component: React.FC<Props> = (props: Props) => {
となっているケースもあり置換後に
function Component(props: Props: Props) {
となってしまっており、下記の置換によって取り除くことができました。
- 検索ワード:
: (\w+):
- 置換ワード:
:
- 対象: 115件、89ファイル
また 2. の置換だとエッジケースとして「props を展開していてかつデフォルト値としてアロー関数が使われている」場合に置換結果がおかしくなってしまったのですが、数が少なかったので一旦 revert して個別に対応することにしました。
// 置換前
export const Modal: React.FC<Props> = ({
open,
onClose = () => {},
}) => {
// 置換後(エラー)
export function Modal({
open,
onClose = (: Props) {},
}) => {
- 修正内容: エラーが出たファイルを revert
- 対象: 4ファイル
4. 残ったファイルを個別に修正
残ったのは15ファイルで、先ほど revert したファイルの他には、主にアロー関数の {}
を書かずに return
を省略しているケースでした。
このケースの数が多ければまた書き換え方法を考えたのですが、ここまでくれば1ファイルずつ書き換えても時間がかからないので、前述の ⌘
+i
→ to function
なども活用しつつ個別に修正しました。
- 修正内容: 個別に修正(Copilot Chat を活用)
- 対象: 15ファイル
これで全てのファイルの修正が完了しました 🎉
おまけ: ts-morph を使ったやり方
ts-morph
を使ったやり方として ChatGPT が以下のようなコードを提案してくれました。
見たところだいぶ惜しいところまでいっていそうなのですが ManipulationError: Manipulation error: Error replacing tree: The children of the old and new trees were expected to have the same count
というエラーが出てしまい失敗しました。
この手の codemod では実装方法を調べたりエッジケースに対応するのにかえって時間がかかってしまうことがありますが、サクッとコードを生成してくれるのであれば便利ですね!
アドベントカレンダーの締め切りがあるので今回は妥協しましたが、時間があるときに再チャレンジしようと思います。
ChatGPT が提案した ts-morph のコード
import { Project, SyntaxKind } from "ts-morph";
const project = new Project();
// 対象ファイルをプロジェクトに追加
project.addSourceFilesAtPaths("apps/client/src/**/*.tsx");
// 対象ファイルを取得
const sourceFiles = project.getSourceFiles();
sourceFiles.forEach((sourceFile) => {
sourceFile.forEachDescendant((node) => {
// 変数宣言を探す
if (node.getKind() === SyntaxKind.VariableDeclaration) {
const variableDeclaration = node.asKind(SyntaxKind.VariableDeclaration)!;
const initializer = variableDeclaration.getInitializer();
// 初期化式がArrowFunctionの場合に処理を進める
if (initializer?.getKind() === SyntaxKind.ArrowFunction) {
const functionNode = initializer.asKind(SyntaxKind.ArrowFunction);
if (!functionNode) return;
// React.FCの型注釈があるか確認
const typeNode = variableDeclaration.getTypeNode();
if (typeNode && (typeNode.getText().startsWith("React.FC") || typeNode.getText().startsWith("FC"))) {
// Props型を取得
const propsType = typeNode.getFirstChildByKind(SyntaxKind.TypeReference)?.getText();
// 新しいfunction宣言を生成
const functionName = variableDeclaration.getName();
const functionBody = functionNode.getBody().getText();
const parameters = functionNode.getParameters().map((param) => param.getText());
const newFunctionText = `function ${functionName}(${parameters.join(
", ",
)}${propsType ? `: ${propsType}` : ""}) ${functionBody}`;
// 元のノードを削除し、新しいノードを挿入
const parent = variableDeclaration.getParent();
parent.replaceWithText(newFunctionText);
}
}
}
});
// ファイルを保存
sourceFile.saveSync();
});
まとめ
わざわざ1から考えるのは億劫な複数行の正規表現の置換が割と短時間でできたので、解決法がゴリ押しであったとしても生成 AI を使うことによってゴリ押しのスピードが格段に上がるため現実的な選択肢となりうると感じました。
今回のようなリファクタリングはプロダクト開発において本質的ではないため、時間がかかるのであれば後回しになりますし実際そうするべきだと思っていますが、ササッと修正できるのであれば選択肢も変わってきて、特にチーム開発においては長期的な開発効率に効いてきそうです。
そういった面で(少なくとも既存のコードと向き合う必要がある現時点では) Copilot Edits や Copilot Workspace を使うことでより複雑なリファクタリングも手間なくできるのではないかと期待を寄せています。
この1年で生成 AI は「一部の人が使っている」から「使わないと生産性で差をつけられる」というフェーズに変わったなと思います。(そして私自身、置いていかれている側だと感じて焦っていますw)
今まであまり ChatGPT を活用できておりませんでしたが「とりあえず ChatGPT に聞いてみる」と思わぬ発見があるなと感じました。
ソフトウェア開発においては、まずとりあえず選択肢を提示してもらい、方針が決まったら追加の質問や従来通りドキュメントを見て内容を固めるのが良さそうです。
スマホのアプリからも ChatGPT Team のライセンスを利用できるので暇なときに何でもかんでも聞いてみてもっと定着させていきたいです。
-
特にテストコードやコメントのサジェストが便利だと感じています ↩︎
-
将来的には大量のファイルを一括に書き換えできるようになると期待しています ↩︎
-
やはり回答にブレがあり、追加の質問で修正も可能ですが1つめの回答の当たり外れに左右されると思いました ↩︎
-
https://code.visualstudio.com/updates/v1_29#_multiline-search ↩︎
Discussion