はじめに
Go や Java を書いていると、VS Code の定義ジャンプで標準ライブラリや外部ライブラリの実装まで読みに行けることがあります。
この記事では、VS Code に次のような拡張機能を入れている前提で整理します。
- Go は Go 拡張と gopls
- Java は Extension Pack for Java
- Vue は Vue - Official
- React / TypeScript は VS Code 標準の TypeScript サポート
例えば fmt.Println や http.Client のメソッドにジャンプすると、実際の .go ファイルに飛びます。
Java でも、JDK のソースや依存ライブラリの sources jar があれば、String や ArrayList、外部ライブラリの .java ソースに飛べます。
一方で、Vue や React を TypeScript で書いていると、同じようにジャンプしても .d.ts に飛ぶことがあります。
declare function inject(...)
このような declare だけの定義に飛んで、「実装はどこにあるのか」と感じることがあります。
この記事では、Go / Java と Vue / React でコードジャンプの体験が違う理由を整理します。
先に結論
Go / Java と Vue / React で定義ジャンプの体験が違うのは、ライブラリの配布形態が違うからです。
大きく見ると、次の違いがあります。
- Go は標準ライブラリも外部ライブラリも
.goのソースコードとして手元に置かれやすい - Java は JDK のソースや sources jar があれば
.javaのソースコードに飛べる - ただし Java は sources jar がない場合、
.classを開いたり、追加の逆コンパイル拡張に頼ったりすることがある - Vue や React は npm パッケージとして、ビルド済み JavaScript や型定義ファイルを中心に配布される
- TypeScript の定義ジャンプは型情報を重視するため、
.d.tsに飛びやすい - ただし、
declarationMapや Go to Source Definition によって元ソースへ飛べる場合もある - アプリケーションコードであれば、Vue や React でも定義ジャンプは普通に使える
- ライブラリ内部を読みたい場合は、npm パッケージではなく GitHub の元ソースを見る方が分かりやすい
つまり、Vue や React で declare だけの型定義に飛ぶのは、エディタが壊れているというより、TypeScript と npm パッケージの世界では自然に起きることです。
Goはソースコードにジャンプしやすい
Go では、標準ライブラリも外部ライブラリも基本的に .go のソースコードとして手元にあります。
標準ライブラリは GOROOT 配下にあります。
go env GOROOT
例えば、標準ライブラリなら次のような場所に実装があります。
$GOROOT/src/fmt/print.go
$GOROOT/src/net/http/client.go
$GOROOT/src/database/sql/sql.go
外部ライブラリも、多くの場合は GOMODCACHE 配下に .go ファイルとして保存されます。
go env GOMODCACHE
例えば、モジュールキャッシュには次のような形で依存ライブラリが置かれます。
$GOMODCACHE/github.com/example/package@v1.2.3/
そのため、VS Code と gopls を使っていると、関数やメソッドの実装にそのままジャンプできます。
fmt.Println("hello")
ここから Println に飛ぶと、型情報だけではなく実装本体を読めます。
Go は「ライブラリの中まで読む」体験がかなり自然です。
Javaもソースがあれば実装にジャンプできる
Java も、ソースが手元にあれば実装にジャンプできます。
JDK には、標準ライブラリのソースが src.zip として含まれていることがあります。
例えば、JDK の中に次のようなファイルがある場合です。
$JAVA_HOME/lib/src.zip
VS Code の Java 拡張がこのソースを認識していれば、String、ArrayList、HashMap、HttpClient などの標準ライブラリの実装にジャンプできます。
外部ライブラリの場合は、Maven や Gradle で依存を取得します。
このとき、通常の jar とは別に sources jar が用意されていることがあります。
例えば Maven のローカルリポジトリでは、次のようなファイルが並ぶことがあります。
~/.m2/repository/com/example/library/1.2.3/library-1.2.3.jar
~/.m2/repository/com/example/library/1.2.3/library-1.2.3-sources.jar
library-1.2.3.jar はコンパイル済みの .class が入った jar です。
library-1.2.3-sources.jar は対応する .java ソースが入った jar です。
VS Code の Java 拡張が sources jar を取得できていれば、外部ライブラリでも実装ソースにジャンプできます。
ただし、sources jar がない場合は少し体験が変わります。
その場合、VS Code では .class を開いたり、ソースを手動で attach したりすることになります。
VS Code では Decompiler for Java のような拡張を使って .class を逆コンパイル表示できることもありますが、sources jar やソースの attach があればより安定して実装を追えます。
Java は Go と違って、依存ライブラリの実体は基本的に jar です。
ソースへ気持ちよくジャンプできるかどうかは、対応するソースが取得できているかに左右されます。
Java
ライブラリ = .jar のバイトコード + sources jar があれば .java ソース
そのため、Java は Go と Vue / React の中間に近いです。
Go ほど常にソースへ直行する感覚ではありませんが、sources jar があれば実装をかなり自然に読めます。
VueやReactは型定義に飛びやすい
一方で、Vue や React は npm パッケージとして配布されます。
npm パッケージには、利用者向けにビルドされた JavaScript や型定義ファイルが入っていることが多いです。
例えば Vue では、次のようなファイルに飛ぶことがあります。
node_modules/@vue/runtime-core/dist/runtime-core.d.ts
.d.ts は TypeScript の型定義ファイルです。
つまり、実装ではなく「この関数はこういう型で存在します」という宣言です。
declare function inject<T>(key: InjectionKey<T> | string): T | undefined
これは実装ではありません。
そのため、定義ジャンプしても処理の中身は読めません。
React でも似たようなことが起きます。
TypeScript で React を書いていると、useState や useEffect にジャンプしたときに、次のような型定義へ飛ぶことがあります。
node_modules/@types/react/index.d.ts
なお、npm パッケージによってはソースコードを同梱しているものもあり、その場合は実装にジャンプできることもあります。
または、次のような入口ファイルに飛ぶこともあります。
node_modules/react/index.js
つまり、Vue だけでなく React でも、ライブラリ内部を読むときは Go ほど素直ではありません。
なぜこの違いが出るのか
違いは、配布されているものの形にあります。
Go
ライブラリ = .go の実装ソース
Java
ライブラリ = .jar のバイトコード + sources jar の .java ソース
Vue / React
ライブラリ = ビルド済み JavaScript + .d.ts 型定義
Vue や React で「ジャンプしたのに declare しかない」と感じるのは、TypeScript が利用者向けの型情報を見ている状態だからです。
ただし、TypeScript でも常に .d.ts しか見られないわけではありません。
ライブラリ側が declarationMap を提供している場合は、.d.ts から元の .ts ソースへ移動できることがあります。
また、VS Code / TypeScript には通常の Go to Definition とは別に Go to Source Definition もあります。
条件が揃えば、.d.ts ではなく実装側の JavaScript / TypeScript を探せることがあります。
アプリコードならReactやVueでも読める
ただし、Vue や React のコードがまったく読めないわけではありません。
自分たちのアプリケーションコードであれば、VS Code のジャンプは普通に使えます。
React の場合は、コンポーネントも関数も基本的に TypeScript / JavaScript として書かれるため、比較的ジャンプしやすいです。
<Button onClick={handleClick} />
このようなコードでは、Button や handleClick にジャンプしやすいです。
Vue も、Vue - Official 拡張を入れていれば、.vue ファイル内のコンポーネント、props、emits、関数定義などにジャンプできます。
<script setup lang="ts">
const handleClick = () => {
console.log("click")
}
</script>
<template>
<button @click="handleClick">click</button>
</template>
ただし、Vue はテンプレートと script が分かれます。
そのため、テンプレートから script 側の定義を追う体験は、React より拡張機能の支援に依存しやすいです。
React は JavaScript / TypeScript のコードとして追う感覚が強く、Vue はテンプレートと script をまたいで追う感覚が強いです。
ライブラリ内部を読むなら元ソースを見る
Vue や React の内部実装を読みたい場合、npm パッケージ内だけを見ると少し読みにくいです。
Vue なら、実装は GitHub 上の元ソースを見る方が読みやすいです。
例えば provide / inject 周りであれば、Vue 本体のソースでは次のような場所にあります。
packages/runtime-core/src/apiInject.ts
React でも同じです。
Context の実装を追いたいなら、npm パッケージの入口ファイルだけでなく、React 本体の元ソースを見る方が分かりやすいです。
例えば createContext 周りであれば、React 本体のソースでは次のような場所にあります。
packages/react/src/ReactContext.js
npm パッケージ内の dist や入口ファイルは、利用者向けに整理された成果物です。
実装 JavaScript が入っていても、ビルド済みの成果物なので、内部実装を学ぶ目的なら GitHub の元ソースを見る方が読みやすいです。
内部実装を学びたい場合は、次のように考えると分かりやすいです。
アプリコードを読む
-> VS Code のジャンプで読む
ライブラリ内部を読む
-> GitHub の元ソースを見る
declareに飛んだら何を見ているのか
declare に飛んだときは、実装ではなく型の入口を見ています。
例えば次のような定義です。
declare function inject<T>(key: InjectionKey<T> | string): T | undefined
これは、「inject という関数があり、このような引数と戻り値を持つ」という宣言です。
実際の処理は別の JavaScript や TypeScript のソースにあります。
そのため、declare に飛んだときは、次のように切り分けるとよいです。
- 使い方や型を知りたいなら
.d.tsを読む - 実装を知りたいなら Go to Source Definition や GitHub の元ソースを見る
- 自分たちのコードの呼び出し元を知りたいなら参照検索を使う
.d.ts は役に立たないファイルではありません。
ライブラリの公開 API を確認するには便利です。
ただし、処理の中身を読むためのファイルではありません。
GoやJava経験者が戸惑いやすいポイント
Go や Java では、定義ジャンプから実装に入れる体験が比較的自然です。
この体験に慣れていると、Vue や React で .d.ts に飛んだときに違和感があります。
ただ、これは Go / Java とフロントエンドの開発体験の差です。
Go はソースコードを読む体験が言語とツールチェーンに強く組み込まれています。
Java は jar で配布されますが、sources jar や JDK ソースによって実装を読める体験が整っています。
Vue や React は、npm、ビルド済み JavaScript、TypeScript の型定義、エディタ拡張が組み合わさって開発体験を作っています。
どちらが正しいというより、前提が違います。
まとめ
Go / Java と Vue / React では、VS Code の定義ジャンプの体験がかなり違います。
整理すると、次のようになります。
Go
標準ライブラリも外部ライブラリも実装に飛びやすい
Java
JDKソースやsources jarがあれば実装に飛びやすい
なければ.classを開くか、ソースのattachや追加拡張を検討する
Vue / React
アプリコードは読める
ライブラリ内部は型定義やビルド済みに飛びやすい
条件が揃えば declarationMap や Go to Source Definition で元ソースへ飛べる
React
アプリコードは比較的ジャンプしやすい
Vue
Vue - Official 拡張が重要
template と script をまたぐため拡張機能への依存が大きい
Vue や React でジャンプしたときに declare しか出てこなくても、それはおかしなことではありません。
アプリコードを書く分には問題なく読めますが、ライブラリ内部を読むときは、型定義、ビルド済み JavaScript、元ソースを切り分けて見る意識が必要です。
Go と TypeScript の型の違いについては、別記事「TypeScriptの型はGoやJavaの型と何が違うのか」でも整理しています。
Vue と React のコードを読む順番の違いについては、別記事「ReactとVueの書き方の違い:同じ画面でも読む順番が変わる」でも整理しています。