TypeScript + React 実践 #1 始まりとセットアップ

読了 8分

TypeScript基礎講座(#1~#7)React基礎講座を終えたなら、いよいよその二つが出会うときです。このシリーズはJavaScriptで書いたReactコードをTypeScriptに移しながらどんな決定をどう下すか、実戦でよく出会うパターンを6編にわたって整理します。

全6編で構成されます。

  • #1 始まりとセットアップ ← 今回
  • #2 propsとchildrenの型付け
  • #3 hooksの型付け (useState/useReducer/useRef)
  • #4 イベントとフォームの型付け
  • #5 Contextとジェネリックコンポーネント
  • #6 fetchとAPIレスポンスの型付け

今回はなぜReactにTypeScriptを使うのか、そしてViteでReact + TSプロジェクトを作って最初のコンポーネントに型を付けるところまでいきます。

なぜReactにTypeScriptなのですか? #

Reactは結局コンポーネントの間にpropsでデータを流すことがすべてと言っても過言ではありません。コンポーネントが増えるほど、次のような質問が絶え間なく登場します。

  • このコンポーネントはどんなpropsを受け取るのか?
  • このpropsは必須か、任意か?
  • onClickはどんな引数を受け取る関数であるべきか?
  • このhookは何を返すのか?

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

TypeScriptが入ると、これらの質問のほとんどがエディタの自動補完と赤線に変わります。

JavaScript — 間違って渡したpropsがランタイムでようやく現れる
function UserCard({ name, age }) {
  return <div>{name} ({age})</div>;
}

// 親コンポーネント
<UserCard name="カーティス" />              // ageが抜けているがそのままレンダリング
<UserCard name="カーティス" age="30" />   // 文字列なのにそのままレンダリング
<UserCard nme="カーティス" age={30} />      // タイプミス — undefinedとして表示
TypeScript — 書く瞬間に捕まる
type UserCardProps = {
  name: string;
  age: number;
};

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

<UserCard name="カーティス" />              // ✗ ageがありません
<UserCard name="カーティス" age="30" />   // ✗ ageはnumberである必要があります
<UserCard nme="カーティス" age={30} />      // ✗ nmeというpropはありません

三つのよくあるミスがすべてエディタで赤線として即座に捕まります。ビルドも止まるので、間違ったコードがユーザーに到達しません。

TypeScriptがReactに与えるもの #

大きく四つに整理できます。

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

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

React + TypeScript環境を最も軽く作る方法はViteです。次のコマンドで5秒以内にプロジェクトが作られます。

新規プロジェクト作成
npm create vite@latest ts-react-playground -- --template react-ts
cd ts-react-playground
npm install
npm run 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を開くと、デフォルトのカウンタページが見えるはずです。

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

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

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

要点は二つです。

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作業で最初に意味があるオプションは二つです。

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

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

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="カーティス" />
    </div>
  );
}

export default App;

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

間違って書く — 赤線を直接見てください
<Hello />                  // ✗ nameが抜けている
<Hello name={42} />        // ✗ stringであるべきだがnumber
<Hello name="カーティス" age={30} /> // ✗ ageというpropは無い

三つともエディタで即座に赤線が引かれ、npm run buildも止まります。これがシリーズ最後まで享受する最も基本的な利点です。

コンポーネントの戻り値の型は普通推論に任せます #

基礎講座で関数に戻り値の型を明示しろと習いましたが、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を強制的に受け取らせるなど小さな不便があるので、このシリーズでも使いません。

よく出会う第一印象 — 赤線が多すぎます #

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

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

2) anyで口を塞ぐのは最後の手段です。 詰まったらとりあえずanyで逃げたい誘惑が出ますが、そこには普通unknownまたはより狭い型がより合っています。anyを使うとそこからすべての自動補完とリファクタリングの安全性が失われます。

まとめ #

今回は次を整理しました。

  • ReactにTypeScriptを使う理由 — コンポーネント契約、自動補完、リファクタリングの安全性
  • Vite + react-tsテンプレートで環境セットアップ
  • .tsx拡張子とstrictモードの意味
  • 最初のコンポーネントにpropsの型を付ける
  • 戻り値の型は推論に任せるのが自然
  • React.FCより({ ... }: Props) => ...パターン

次の記事(#2 propsとchildrenの型付け)ではpropsの型付けをより深く扱います。オプショナルprop、union prop、childrenパターン、そして合成コンポーネントで型をどう流すかまで扱います。

X