mizdra's blog

ぽよぐらみんぐ

React コンポーネントの定義の仕方によって VSCode の定義元ジャンプの挙動が変わる

この記事は「はてなエンジニア Advent Calendar 2022」の3日目の記事です。2日目は id:pokutuna さんの「Slack チャンネルのロボット帝国化を防ぐ feed-pruning-proxy」でした。

blog.pokutuna.com


さて、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 の定義元へジャンプする機能の挙動が他と違います 。具体的には、他のスタイルと比べてジャンプ先を選択するステップが挟まり、ワンクリックでジャンプできないようになってます。

youtu.be

皆さんご存知でしたか?

お試し会場

(利用するエディタによって再現できるかどうか変わってくるのかもしれないですが) この問題は VSCode だけでなく、Codesandbox でも再現する問題です。

という訳で以下に Codesandbox で動く環境を用意してみました。こちらから皆さんもお試しください:

お手元の VSCode で試したい方は、以下のリポジトリから git clone してみてください。

github.com

一体何が起きているのか

定義元ジャンプをしようとした時の画面をよく見てみると、我々が期待しているジャンプ先 (const Button3 = ... の行) に加えて、 node_modules/@types/react にある FunctionComponent (React.FC の別名) の型定義の行も候補として出ています。

3番目の書き方で定義元が 2 つ表示されている様子

原因はよくわからないのですが、どうやら const Button3 = ...node_modules/@types/react 側の React.FC の型定義の両方が<Button3> の定義元として認識されてしまっているようです。定義元が複数あるので、どちらにジャンプするかを開発者に選択させる必要があり、あのようなステップが挟まってしまっているようです。

正直これがバグなのか、仕様なのか id:mizdra には判断つかなかったのですが、公式 Issue では Bug ラベルが付いていたので、一応バグとして扱われているようです。

github.com

回避策

実際のところ、node_modules/@types/react 側の React.FC の型定義を開発者が見たいことはないですし、この挙動は煩わしいでしょう。そこでこの問題の回避策をいくつか紹介したいと思います。

複数の定義元を選択させる UI が出たら、Enter を押す

回避策...というより対症療法的な話ですが、複数の定義元を選択させる UI が出た際に Enter を押せば、すぐに期待している定義元へとジャンプできます。

Enter を押すと即座に期待している定義元へとジャンプできる。

VSCode の Editor > Goto Location: Multiple Definitionsgoto にする

Editor > Goto Location: Multiple Definitions という設定を弄ると、複数の定義元があった時の VSCode の定義元ジャンプの挙動を変更できます。

デフォルトの設定値は peek となっていて、gotogotoAndPeek に変更すると、ワンクリックでジャンプできるようになります。

  • peek: どの定義元にジャンプするか選択する UI を表示 => クリックでジャンプ
  • goto: 最も優先度の高い定義元にジャンプする
    • const Button3 = ... が最も優先度の高い定義元なようで、ワンクリックでジャンプできる
  • gotoAndPeek: 最も優先度の高い定義元にジャンプし、ジャンプ先で peek と同じ UI を表示

この設定は React コンポーネント以外の定義元ジャンプでも使われる、影響範囲の広い設定です。もしかしたら goto にしてしまうことで、peek のように候補を選んでジャンプしたかったのに、それができない…というケースがあるかもしれません。

一応「左クリックメニュー > ピーク > 定義をここに表示」から、従来のように定義元を複数表示することができます。もし定義元を複数をたければ、これを使うと良いと思います。

「左クリックメニュー > ピーク > 定義をここに表示」から定義元を複数表示できる

vscode-tsx-arrow-definition を使う

node_modules/@types/react を候補先から除外して、ワンクリックでジャンプできるようにする VSCode 拡張機能があるようです。

marketplace.visualstudio.com

これだと手軽ですし、影響範囲も 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 で返り値の型を補完できるので、それほどストレスは感じてないです。

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>;

一見するとワンクリックで定義元へとジャンプできるように見えますが…実はできません。

satisfies を使っても、ワンクリックで定義元へとジャンプできない。

こちらは 「アロー関数 + 変数宣言に型注釈」 と違って、「Button4({ children }) => { ... } が定義元である」と認識されているようです。node_modules/@types/react の型定義は出てこなくなったものの、satisfies が新たに定義元を作り出してしまい、ワンクリック挟まる形になってしまいます。上手くいかないものですね。

まとめ

  • 「アロー関数 + 変数宣言に型注釈」スタイルだとワンクリックで定義元にジャンプできない
  • VSCode の設定を変える/VSCode 拡張機能を使う/他のスタイルでコンポーネントを定義する、などで回避可能

はてなエンジニア Advent Calendar 2022 の明日の担当は id:happy_siro さんです!

ポケットモンスター・ポケモン・Pokémon・は任天堂・クリーチャーズ・ゲームフリークの登録商標です.

当ブログは @mizdra 個人により運営されており, 株式会社ポケモン及びその関連会社とは一切関係ありません.