目次
16 章

TypeScript + React セットアップ

Vite + TypeScript セットアップ、tsconfig の主要オプション、最初の .tsx ファイルまで。本書3部の土台を作ります。

15章まででで2部が締めくくられました。1 〜 2部ではコンポーネント・props・state・イベント・フォーム・useEffect・状態のリフトアップ・Context・カスタムフック・パフォーマンス・ルーティングまでをすべて手に馴染ませてきました。本章から3部が始まります。これまで見てきたコードを TypeScript の上に載せ直していきます

本書のすべての例コードは実は最初から TypeScript で書くこともでき、本書が掲げるモデルは「TypeScript 優先」です。1 〜 2部で JavaScript で始めたのは、React の核心概念に先に集中するためでした。ここで TypeScript の安全網をかぶせていくと、3部以降(Next.js・RSC・Server Actions・フルスタック)のすべてのコードが自然に TypeScript の上で流れていきます。

なぜ React に TypeScript を使うのか #

React は結局のところ コンポーネント間で props を通じてデータを流すこと がほぼすべてだと言っても過言ではありません。コンポーネントが増えるにつれて、次のような質問が絶えず出てきます。

  • このコンポーネントはどんな props を受け取るのか
  • この props は必須か、選択か
  • onClick はどんな引数を受け取る関数であるべきか
  • このフックは何を返すのか

JavaScript で書くときは、コードを遡ってコンポーネント本体を直接読んだり、コンソールに出力してみたり、誤って渡していないかを画面で確認したりします。小さなアプリならそれで十分ですが、コンポーネントが 50 個を超えるとコストが急速に積み上がります。

TypeScript が入ってくると、これらの質問の大半が エディタの自動補完と赤線 に置き換わります。

JavaScript — 誤った props はランタイムにならないと露見しない
function UserCard({ name, age }) {
  return <div>{name} ({age})</div>;
}

// 親コンポーネント
<UserCard name="Curtis" />              // age が抜けたまま、そのままレンダリング
<UserCard name="Curtis" age="三十" />   // 文字列なのにそのままレンダリング
<UserCard nme="Curtis" age={30} />      // タイポ — undefined と表示
TypeScript — 書いたその瞬間に検出される
type UserCardProps = {
  name: string;
  age: number;
};

function UserCard({ name, age }: UserCardProps) {
  return <div>{name} ({age})</div>;
}

<UserCard name="Curtis" />              // ✗ age がありません
<UserCard name="Curtis" age="三十" />   // ✗ age は number でなければなりません
<UserCard nme="Curtis" age={30} />      // ✗ nme という prop はありません

3 つのよくある間違いがすべて エディタ上で赤線として即座に 検出されます。ビルドも止まるので、誤ったコードがユーザーまで届きません。

TypeScript が React に与えるもの #

大きく 4 点に整理できます。

  1. コンポーネントの契約(contract) — props がどんな形なのかをコードで明示し、呼び出し側でその場で検証されます。
  2. 自動補完event.target.valueuseState が返すタプル、フック結果のオブジェクトがすべて推論され、エディタが自動補完してくれます。
  3. リファクタの安全性 — props 名を変えたりフィールドを追加・削除すると、それを使っているすべての場所が一度に赤線で表示されます。
  4. ドキュメント不要のコード — コンポーネントのシグネチャを見るだけで使い方が分かるので、別途のコメントやドキュメントへの依存度が下がります。

セットアップ — Vite で React + TS を始める #

React + TypeScript 環境をもっとも軽く整える方法は Vite です。2章で整えた環境の上で、--template react-ts オプション一つを追加するだけです。

新規プロジェクト作成
pnpm create vite@latest ts-react-playground --template react-ts
cd ts-react-playground
pnpm install
pnpm dev

--template react-ts が肝です。Vite が次を自動でセットアップしてくれます。

  • tsconfig.jsontsconfig.app.jsontsconfig.node.json
  • React 19 向けの @types/react@types/react-dom
  • .tsx 拡張子と strict モード
  • ESLint + TypeScript ルール一式

ブラウザで http://localhost:5173 を開くと、基本のカウンタページが表示されます。

本書 1 〜 2部の例コードをすでに作ったプロジェクトがあるなら、そこに TypeScript を段階的に導入することもできます。とはいえ新しいプロジェクトで始める方がきれいなので、本章からは上の新しいプロジェクトで進めることを推奨します。

プロジェクト構成を見てみる #

生成されたプロジェクトの主要ファイルは次の通りです。

ts-react-playground/
├── src/
│   ├── App.tsx          # メインコンポーネント(.tsx 拡張子に注目)
│   ├── main.tsx         # エントリーポイント
│   ├── App.css
│   └── vite-env.d.ts    # Vite 環境変数の型宣言
├── index.html
├── tsconfig.json
├── tsconfig.app.json    # アプリコード用のコンパイル設定
├── tsconfig.node.json   # vite.config.ts 用の設定
├── vite.config.ts
└── package.json

ポイントは 2 つです。

1) .tsx 拡張子 — JSX を含む TypeScript ファイルは .ts ではなく .tsx です。JSX 構文を認識させるためにコンパイラが知る必要があり、拡張子で区別します。

2) strict モード — Vite の react-ts テンプレートは既定で "strict": true が有効です。これが有効になっていないと TypeScript の保護幕が意味を持ちません。最初は赤線が多くて息苦しいかもしれませんが、絶対にオフにしないでください。

tsconfig.app.json を開いてみると、次のような行が見えます。

tsconfig.app.json(抜粋)
{
  "compilerOptions": {
    "target": "ES2022",
    "useDefineForClassFields": true,
    "lib": ["ES2022", "DOM", "DOM.Iterable"],
    "module": "ESNext",
    "jsx": "react-jsx",
    "strict": true,
    "noUnusedLocals": true,
    "noUnusedParameters": true,
    "noFallthroughCasesInSwitch": true,
    "moduleResolution": "bundler"
  },
  "include": ["src"]
}

このうち、React 作業でもっとも先に意味を持つオプションは 2 つです。

  • "jsx": "react-jsx" — React 17 以降の新しい JSX 変換を使います。コンポーネントファイルの先頭に毎回 import React from 'react' と書かなくて済みます。
  • "strict": truestrictNullChecksnoImplicitAny など、型安全性の主要フラグをすべて有効にします。

本書のすべてのコードは strict モードを前提にしています。

最初のコンポーネントに型を付ける #

src/App.tsx を開き、生成されたコードをすべて削除して、次の通りに書き換えてみましょう。

src/App.tsx — Hello コンポーネント
type HelloProps = {
  name: string;
};

function Hello({ name }: HelloProps) {
  return <h1>こんにちは、{name}さん!</h1>;
}

function App() {
  return (
    <div>
      <Hello name="Curtis" />
    </div>
  );
}

export default App;

保存するとブラウザに「こんにちは、Curtis さん!」が表示されます。今度はわざと誤って呼び出してみましょう。

誤った呼び出し — 赤線を実際に見てみてください
<Hello />                          // ✗ name が抜けている
<Hello name={42} />                // ✗ string であるべきなのに number
<Hello name="Curtis" age={30} />   // ✗ age という prop はない

3 つすべてがエディタ上で即座に赤線になり、pnpm build も止まります。これが本書3部で享受することになるもっとも基本的な利点です。

コンポーネントの戻り値型は通常推論に任せる #

関数には戻り値の型を明示せよと頻繁に教わりますが、React の関数コンポーネントは通常明示しません。推論が十分に効きますし、明示するとかえって表現力が落ちる場面が多いからです。

過度な明示 — 推奨しない
function Hello({ name }: HelloProps): React.ReactElement {
  return <h1>こんにちは、{name}さん!</h1>;
}

React.ReactElement に固定すると、後で条件付きで null を返したり fragment を返したくなったときに型が合わず、再び手を入れる羽目になります。推論に任せれば次のすべてを自由に返せます。

推論を信頼 — 自然に
function Hello({ name, hidden }: { name: string; hidden?: boolean }) {
  if (hidden) return null;                          // OK
  return <h1>こんにちは、{name}さん!</h1>;          // OK
}

React 19 向けの @types/react は、コンポーネントが返せるすべての形(エレメント、文字列、null、fragment など)を自動で推論してくれます。

注記
古い資料では React.FC<Props> を使えとよく勧められます。最近のコミュニティは そのまま ({ ... }: Props) => ... パターンを使う方向 に落ち着きました。FC は children を強制的に受け取らせるなど小さな不便があり、本書でも使いません。付録 A(旧 React マイグレーション)で FC から関数 + props 分割代入への移行手順を扱います。

よくある第一印象 — 赤線が多すぎる #

JavaScript から TypeScript に移ると、最初は赤線との戦いのように感じることがあります。慣れるまで覚えておくとよい 2 点があります。

1) 赤線は敵ではなく仲間です。 その一つ一つが「今このコードのままならランタイムに検出されたはずのバグ」を前もって見せてくれているものです。最初の 1 〜 2 週間は息苦しくても、時間が経つと「あれ、赤線が出ない。間違ってないだろうか」という側に感覚が変わります。

2) any で口を塞ぐのは最後の手段です。 詰まったらとりあえず any で逃がしたい誘惑に駆られますが、そういう部分には大抵 unknown やもっと狭い型がより適しています。any を使うと、その部分からすべての自動補完とリファクタの安全性が消えます。本書の例コードでは any を使いません。

自分でやってみる #

1 〜 2部で作ったコンポーネントをいくつか TypeScript で書き直してみましょう。

src/Counter.tsx:

src/Counter.tsx
import { useState } from 'react';

type CounterProps = {
  initial?: number;
};

function Counter({ initial = 0 }: CounterProps) {
  const [count, setCount] = useState(initial);

  return (
    <div style={{ padding: '16px', border: '1px solid #ccc', borderRadius: '8px' }}>
      <h2>カウント: {count}</h2>
      <button onClick={() => setCount(prev => prev + 1)}>+1</button>
      <button onClick={() => setCount(prev => prev - 1)}>-1</button>
      <button onClick={() => setCount(initial)}>リセット</button>
    </div>
  );
}

export default Counter;

src/App.tsx:

src/App.tsx
import Counter from './Counter';

function App() {
  return (
    <>
      <h1>TypeScript カウンタ</h1>
      <Counter />
      <Counter initial={10} />
    </>
  );
}

export default App;

保存してブラウザで動作を確認します。その後、<Counter initial="10" /> のように誤った型を渡してみてください。エディタに赤線が出て pnpm build も止まります。

練習問題 #

  1. Counterinitial prop を必須に変えて(initial: number)、親から呼び出すときに <Counter /> のように抜かしてみてください。赤線がどう出るかを確認します。
  2. useState(0) の推論された型をエディタで確認してください(VS Code で変数にマウスホバー)。count: numbersetCount: Dispatch<SetStateAction<number>> が見えます。次に useState('hello') に変えると、count: string に推論が追従するのを観察します。
  3. Counter コンポーネントの中にわざと (initial as any).toFixed(2) のような any の使用を入れてみてください。その部分から自動補完とリファクタの安全性がどう消えるかを確認します。次に unknown に変えてみて、コンパイラがナローイング(typeof initial === 'number')を強制するのを観察します。

一行まとめ: 本書3部は TypeScript 優先。Vite + react-ts テンプレートで始め、.tsx 拡張子と strict モードを既定にする。最初のコンポーネントの props は type で定義し、戻り値の型は推論に任せる。React.FC の代わりに ({ ... }: Props) => ... パターンを使う。any は最後の手段で、赤線は敵ではなく仲間。

次の章 #

次の 17章 props と children のタイピングでは、props のタイピングをさらに深く扱います。選択 prop、ユニオン prop、ComponentProps<'button'> で HTML 属性を受け取る方法、ReactNodeReactElement の違い、PropsWithChildren をいつ使うかまでを一度に扱います。

X