😼

TypeScript/Reactのimportがスッキリする!index.tsの仕組み

に公開3

はじめに

React や TypeScript でプロジェクトを作っていると、こんなimport文を見たことありませんか?

// 一行でたくさんのコンポーネントをimport
import { Button, Card, Modal } from './components';
import { useAuth, useApi } from './hooks';

「あれ?componentsフォルダにはButton.tsx、Card.tsx、Modal.tsxって個別のファイルがあるはずなのに、なんで一行でimportできるの?」

その謎を解く鍵が index.ts ファイルなんです!

今回は、この便利な仕組みを解説します。

index.tsって何?

基本的な仕組み

index.ts は、フォルダの中身をまとめて外部に公開するための「入口」ファイルです。

例えば、こんなフォルダ構成があったとします:

components/
├── Button.tsx
├── Card.tsx
├── Modal.tsx
└── index.ts  ← これが入口ファイル

この index.ts の中身:

components/index.ts
export { default as Button } from './Button';
export { default as Card } from './Card';
export { default as Modal } from './Modal';

すると、外部からは:

// ✅ フォルダ名だけで一括import可能!
import { Button, Card, Modal } from './components';

これができるようになります。

なぜこんなことができるの?

実は、Node.js には「フォルダをimportすると自動的に index.js(または index.ts)を探す」という仕様があります。

つまり:

  • import ... from './components' と書く
  • → Node.js が ./components/index.ts を探す
  • → 見つかったら、そのファイルの内容をimportする

これが自動で行われているんです!

実際の例

Before: index.tsがない場合

// ❌ 個別にファイルパスを指定する必要がある
import Button from './components/Button';
import Card from './components/Card';
import Modal from './components/Modal';
import { useAuth } from './hooks/useAuth';
import { useApi } from './hooks/useApi';

After: index.tsがある場合

components/index.ts
// components/index.ts
export { default as Button } from './Button';
export { default as Card } from './Card';
export { default as Modal } from './Modal';
hooks/index.ts
// hooks/index.ts
export { useAuth } from './useAuth';
export { useApi } from './useApi';
// ✅ スッキリ!
import { Button, Card, Modal } from './components';
import { useAuth, useApi } from './hooks';

実践例:ストップウォッチアプリ

実際のプロジェクトで見てみましょう。ストップウォッチアプリの構成です:

stopwatch/
├── page.tsx
├── layout.tsx
├── components/
│   ├── Stopwatch.tsx
│   └── index.ts
├── hooks/
│   ├── useStopwatch.ts
│   └── index.ts
└── ui/
    ├── TimeDisplay.tsx
    ├── ControlButtons.tsx
    ├── StatusIndicator.tsx
    └── index.ts

各 index.ts の中身

components/index.ts
// メインコンポーネントをエクスポート
export { default as Stopwatch } from './Stopwatch'; 
hooks/index.ts
// カスタムフックをエクスポート
export { useStopwatch } from './useStopwatch'; 
ui/index.ts
// UIコンポーネントをまとめてエクスポート
export { default as TimeDisplay } from './TimeDisplay';
export { default as ControlButtons } from './ControlButtons';
export { default as StatusIndicator } from './StatusIndicator';

page.tsx での使用

page.tsx
// スッキリとしたimport!
import { TimeDisplay, ControlButtons, StatusIndicator } from './ui';
import { useStopwatch } from './hooks';

export default function StopwatchPage() {
  // コンポーネントを使用
  return (
    <div>
      <TimeDisplay time="00:00.00" />
      <ControlButtons onStart={handleStart} onStop={handleStop} />
      <StatusIndicator isRunning={false} />
    </div>
  );
}

index.ts を使うメリット

1. importがスッキリする

// Before: 長くて読みにくい
import TimeDisplay from './ui/TimeDisplay';
import ControlButtons from './ui/ControlButtons';
import StatusIndicator from './ui/StatusIndicator';

// After: 短くて分かりやすい
import { TimeDisplay, ControlButtons, StatusIndicator } from './ui';

2. ファイル構成の変更に強い

ファイルの場所が変わっても、index.ts を更新するだけでOK:

// components/index.ts を更新するだけ
export { default as Button } from './buttons/Button';  // 場所変更
export { default as Card } from './Card';

3. 外部に公開したいものを制御できる

// index.ts で公開するものを選択
export { default as Button } from './Button';        // 公開
export { default as Card } from './Card';            // 公開
// InternalComponent は export しない → 外部から使えない

4. チーム開発で統一感が出る

みんなが同じ方法でimportするので、コードの一貫性が保たれます。

ベストプラクティス

1. 関心事ごとにフォルダを分ける

src/
├── components/     # 再利用可能なコンポーネント
│   └── index.ts
├── hooks/         # カスタムフック
│   └── index.ts
├── utils/         # ユーティリティ関数
│   └── index.ts
└── types/         # TypeScript型定義
    └── index.ts

2. 分かりやすいコメントを書く

ui/index.ts
/**
 * UI コンポーネントのエクスポート
 * 
 * ストップウォッチアプリで使用する全てのUIコンポーネントを
 * 一箇所からまとめてエクスポート
 */

// 時間表示コンポーネント(MM:SS.CC形式での時間表示)
export { default as TimeDisplay } from './TimeDisplay';

// 操作ボタンコンポーネント(開始・停止・リセット)
export { default as ControlButtons } from './ControlButtons';

3. 命名規則を統一する

// ファイル名: PascalCase
Button.tsx
UserCard.tsx

// フォルダ名: kebab-case または camelCase
components/
user-profile/

注意点

1. 循環参照に注意

// ❌ A が B を、B が A を参照すると循環参照になる
// A/index.ts
export { B } from '../B';

// B/index.ts  
export { A } from '../A';  // 循環参照!

2. 必要以上に深い階層は避ける

// ❌ 深すぎる
import { Button } from './components/ui/buttons/primary';

// ✅ 適切な深さ
import { Button } from './components';

まとめ

index.ts は、フォルダの内容をまとめて公開するための「入口」ファイルです。

覚えておくポイント

  1. Node.js が自動的に index.ts を探してくれる
  2. import文がスッキリして読みやすくなる
  3. 関心事ごとにフォルダを分けて、それぞれに index.ts を配置
  4. 外部に公開したいものだけを選んでexportできる

最初は「なんで?」と思うかもしれませんが、使ってみると本当に便利です。

特に大きなプロジェクトになると、この仕組みがあるかないかで開発効率が大きく変わります。

ぜひ次のプロジェクトで試してみてください!


参考リンク

この記事が「index.tsって何?」という疑問の解決に役立ったら嬉しいです!

Discussion

Honey32Honey32

失礼します。話の腰を折るような形になってしまい恐縮ですが、

/components や /hooks みたいな単位で index.ts を用意するのは「バレルファイル」というアンチパターンに該当します。

/components/table/ の中に table-row, table-header のようなコンポーネントが含まれていて、それらをまとめて Table.Row Table.Header みたいに利用可能にするような形なら、「index.ts がカプセル化を担う」ことになるので有益ですが、

/components /hooks のような「関連の無いものを、分類して一箇所にまとめている」ディレクトリでは却ってプロジェクト全体のソースコードの依存関係が複雑になります。


よくまとまった説明が無いので端的に説明すると、ファイルの「インポートする / される」関係のグラフを描いたときに「機能性凝集」と似たような構造になり、それによって、「この記述は、バンドルに含まれるか含まれないか」の予測しづらくなります。

すると、

  • バンドルサイズが予期せず膨らむ
  • RSC(React Server Component)でサーバー / クライントで処理を分離したいのに、両者が混ざってしまう。

といった状態におちいる危険性が増します。

碧月碧月

コメントありがとうございます!

アンチパターンがあるのですね…!

/components/hooks といった単位での index.ts の使用が、状況によっては依存関係の複雑化やバンドルサイズの問題に繋がる可能性があるというご指摘は、まだ理解が浅い部分でしたので、大変勉強になりました。

また、RSCでサーバー用とクライアント用のコードが意図せず混ざってしまうことで、「ビルドエラーや不要コードの混入といった問題が起こりやすくなる」ということを理解しました。

ありがとうございます!

Honey32Honey32

いえいえ、少しでもお役に立てたなら幸いです!