追記 (2024-05-13)
この不具合は #57969 で修正されました。まだ Stable 版には入ってませんが、Nightly 版には既に取り込まれています。実際に TypeScript Playground で Nightly 版を使ってみたところ、コンポーネントを定義している箇所へワンクリックでジャンプできるようになっていました。
この記事は「はてなエンジニア Advent Calendar 2022」の3日目の記事です。2日目は id:pokutuna さんの「Slack チャンネルのロボット帝国化を防ぐ feed-pruning-proxy」でした。
さて、TypeScript で React コンポーネントを定義する時、皆さんはどういう書き方をしてますか? 関数宣言/アロー関数どちらを使って書くか、React.FC
を使うかどうか、など微妙に人によって書き方が異なると思います。
その中でも、よく使われるのは以下の 3 つのスタイルでしょうか。
import React from "react"; type ButtonProps = { children: React.ReactNode; }; // 関数宣言。 function Button1({ children }: ButtonProps) { return <button>{children}</button>; } // アロー関数 + 引数リストに型注釈。 const Button2 = ({ children }: ButtonProps) => { return <button>{children}</button>; } // アロー関数 + 変数宣言に型注釈。 // 返り値の型が `JSX.Element | null` に制限されるという点で、 // `0` などを返される心配がなくて安全。 const Button3: React.FC<ButtonProps> = ({ children }) => { return <button>{children}</button>; };
型の付け方や厳密さ、使っている構文が違うくらいで、どれも普通に Button コンポーネントとして使えます。最後の「アロー関数 + 変数宣言に型注釈」スタイルだと返り値の型が厳密になるので、このスタイルで書いている方は結構いらっしゃるのではないかと思います。
...しかしこの最後の「アロー関数 + 変数宣言に型注釈」スタイルは、VSCode の定義元へジャンプする機能の挙動が他と違います 。具体的には、他のスタイルと比べてジャンプ先を選択するステップが挟まり、ワンクリックでジャンプできないようになってます。
皆さんご存知でしたか?
お試し会場
(利用するエディタによって再現できるかどうか変わってくるのかもしれないですが) この問題は VSCode だけでなく、Codesandbox でも再現する問題です。
という訳で以下に Codesandbox で動く環境を用意してみました。こちらから皆さんもお試しください:
お手元の VSCode で試したい方は、以下のリポジトリから git clone してみてください。
一体何が起きているのか
定義元ジャンプをしようとした時の画面をよく見てみると、我々が期待しているジャンプ先 (const Button3 = ...
の行) に加えて、 node_modules/@types/react
にある FunctionComponent
(React.FC
の別名) の型定義の行も候補として出ています。
原因はよくわからないのですが、どうやら const Button3 = ...
と node_modules/@types/react
側の React.FC
の型定義の両方が<Button3>
の定義元として認識されてしまっているようです。定義元が複数あるので、どちらにジャンプするかを開発者に選択させる必要があり、あのようなステップが挟まってしまっているようです。
正直これがバグなのか、仕様なのか id:mizdra には判断つかなかったのですが、公式 Issue では Bug
ラベルが付いていたので、一応バグとして扱われているようです。
回避策
実際のところ、node_modules/@types/react
側の React.FC
の型定義を開発者が見たいことはないですし、この挙動は煩わしいでしょう。そこでこの問題の回避策をいくつか紹介したいと思います。
複数の定義元を選択させる UI が出たら、Enter を押す
回避策...というより対症療法的な話ですが、複数の定義元を選択させる UI が出た際に Enter を押せば、すぐに期待している定義元へとジャンプできます。
VSCode の Editor > Goto Location: Multiple Definitions
を goto
にする
Editor > Goto Location: Multiple Definitions
という設定を弄ると、複数の定義元があった時の VSCode の定義元ジャンプの挙動を変更できます。
デフォルトの設定値は peek
となっていて、goto
か gotoAndPeek
に変更すると、ワンクリックでジャンプできるようになります。
peek
: どの定義元にジャンプするか選択する UI を表示 => クリックでジャンプgoto
: 最も優先度の高い定義元にジャンプするconst Button3 = ...
が最も優先度の高い定義元なようで、ワンクリックでジャンプできる
gotoAndPeek
: 最も優先度の高い定義元にジャンプし、ジャンプ先でpeek
と同じ UI を表示
この設定は React コンポーネント以外の定義元ジャンプでも使われる、影響範囲の広い設定です。もしかしたら goto
にしてしまうことで、peek
のように候補を選んでジャンプしたかったのに、それができない…というケースがあるかもしれません。
一応「左クリックメニュー > ピーク > 定義をここに表示」から、従来のように定義元を複数表示することができます。もし定義元を複数をたければ、これを使うと良いと思います。
vscode-tsx-arrow-definition を使う
node_modules/@types/react
を候補先から除外して、ワンクリックでジャンプできるようにする VSCode 拡張機能があるようです。
これだと手軽ですし、影響範囲も React コンポーネントの定義元ジャンプに絞れるので良さそうですね。
他のスタイルで React コンポーネントを定義する
VSCode の設定や VSCode の拡張機能に頼る形だと、特定のエディタでだけ対応する形になって好きじゃないな…という理由で id:mizdra は「関数宣言」スタイルで書くようにしています。「アロー関数 + 変数宣言に型注釈」スタイルと比較すると、返り値の型は厳密に扱えませんが...多くの場合コンポーネントを利用する側から型違反として検出できるので、問題ないと判断してます。
import React from 'react'; // 誤って number 型を返しているコンポーネント function Button() { return 0; } function App() { return ( <div> <Button /> // ^^^^^^ // 'Button' cannot be used as a JSX component. // Its return type 'number' is not a valid JSX element.(2786) </div> ) }
関数の返り値の型を明示的に書く規約を取り入れているプロジェクトでもこのスタイルで書いてますが、VSCode の Infer function return type
で返り値の型を補完できるので、それほどストレスは感じてないです。
おまけ: satisifies
を使って React コンポーネントを定義する
TypeScript 4.9 で追加された satisifies
を使うと、React.FC
型と互換性を持たせつつ、より厳密な型の値を定義できます (id:gfx さんより情報提供いただきました。ありがとうございます!)。
// アロー関数 + satisfiesで制約を加えつつ正確な型を表現する。 // 安全だが関数本体をカッコで囲む必要がある。また TypeScript 4.9 時点では定義元ジャンプもワンクッションあり。 // Button3 だと返り値の型が `JSX.Element | null` になったが、こちらの書き方だと返り値の型の推論結果が優先されて `JSX.Element` になる。 const Button4 = (({ children }) => { return <button>{children}</button>; }) satisfies React.FC<ButtonProps>;
一見するとワンクリックで定義元へとジャンプできるように見えますが…実はできません。
こちらは 「アロー関数 + 変数宣言に型注釈」 と違って、「Button4
と ({ children }) => { ... }
が定義元である」と認識されているようです。node_modules/@types/react
の型定義は出てこなくなったものの、satisfies
が新たに定義元を作り出してしまい、ワンクリック挟まる形になってしまいます。上手くいかないものですね。
まとめ
- 「アロー関数 + 変数宣言に型注釈」スタイルだとワンクリックで定義元にジャンプできない
- VSCode の設定を変える/VSCode 拡張機能を使う/他のスタイルでコンポーネントを定義する、などで回避可能
はてなエンジニア Advent Calendar 2022 の明日の担当は id:happy_siro さんです!