typescript with react 実践ガイド:導入から型安全な開発まで

この記事ではReactでTypeScriptを導入する手順(新規/既存プロジェクト)から、コンポーネントや各種フック(useState/useReducer/useContext/useMemo/useCallback)の型付け、DOMイベント・children・style propsなど便利な型まで具体例で解説し、型エラーや設計の迷いを減らせます。

目次

React×TypeScriptの概要と得られるメリット

react+typescript+typesafety

「typescript with react」は、UI開発の柔軟さで知られるReactに、TypeScriptの静的型付けを組み合わせて開発品質を高めるアプローチです。Reactはコンポーネントベースで変更に強い一方、JavaScriptのみだと実装規模が大きくなるにつれて「想定していないデータが流れ込む」「リファクタで壊れる」といった問題が表面化しがちです。そこでTypeScriptを併用することで、コードの意図を型として明示し、実装・レビュー・保守の各局面での手戻りを減らせます。

ReactでTypeScriptを使うと何が良いのか

React開発にTypeScriptを導入する最大の価値は、「コンポーネント境界(Props)」「状態(State)」「イベント」「外部APIレスポンス」など、バグが起きやすい接点を型で固定できる点にあります。結果として、実装時の安心感だけでなく、将来の変更耐性が大きく向上します。

  • リファクタリングの安全性が上がる:Props名の変更や型の変更があった際、影響範囲がコンパイルエラーとして可視化されます。Reactはコンポーネント分割が進むほど依存関係が増えますが、型が「変更の地図」になります。

  • コンポーネントの利用方法が自己文書化される:Propsの型定義が「このコンポーネントに何を渡すべきか」を明確にし、実装者以外でも正しい使い方に到達しやすくなります。

  • IDE補完が強くなり生産性が上がる:TypeScriptの型情報により補完・ジャンプ・リネームが高精度になります。特に大規模なReactコードベースでは探索コスト削減が効きます。

  • 実行時エラーを事前に減らせる:たとえば「null/undefinedの可能性」「プロパティの取り違い」などを、実行前に検知できます。UIはユーザー操作・非同期処理が多く、実行時にしか露呈しない不具合が起きやすい領域ですが、型が予防線になります。

  • チーム開発での合意形成がしやすい:APIの形、コンポーネント設計の契約(Contract)を型として共有でき、レビューで「意図の解釈違い」が起きにくくなります。

導入前に押さえるべき判断ポイント

一方で、typescript with reactは「入れれば自動的に良くなる」ものではありません。導入にはコストが伴い、チームやプロジェクトの状況次第で負担が勝るケースもあります。ここでは導入判断に直結する論点を整理します。

ビルド手順が増えて複雑になりやすい

TypeScriptは型チェックやトランスパイルが絡むため、プロジェクトのビルド・検証フローが増えがちです。React単体の開発では見えていなかった「型チェックの失敗」「設定差分」「CIでのみ落ちる」といった問題が発生する可能性があります。

  • 設定(tsconfigなど)の理解が必要になり、最初は「なぜ通らないのか」が分かりづらいことがあります。

  • 型チェックを厳格にすると品質は上がる一方、初期移行時はエラーの山ができて進行が止まりやすくなります。

  • ビルド時間・CI時間の増加がボトルネックになる場合もあるため、チームの開発速度とのバランス設計が重要です。

導入時は、品質向上のメリットと運用コスト(設定・実行時間・トラブルシュート)を天秤にかけ、段階的に厳格化する前提で進めるのが現実的です。

Reactや状態管理の理解が前提になりやすい

TypeScriptは「正しく型付けするほど強力」ですが、Reactの設計意図が曖昧なまま型だけを付けると、かえって複雑な型が増殖しがちです。特にStateやPropsの責務が整理されていないと、型定義もブレて保守性が落ちます。

  • コンポーネントの責務分割が適切でないと、Propsが肥大化し、型も読みづらくなります。

  • 状態の持ち方(どこにStateを置くか)が定まっていないと、型が「場当たり的なつぎはぎ」になりやすいです。

  • 型で無理やり整合性を取ろうとして設計課題が隠れることがあるため、型を付ける前にReactの基本設計を整える意識が重要です。

型システムの学習コストが発生する

TypeScriptはJavaScriptの上位互換とはいえ、型の考え方に慣れるまで一定の学習コストが発生します。とくにReact特有のパターン(Props、children、イベント、外部データの取り扱いなど)では、型推論と明示指定の使い分けを理解する必要があります。

  • チーム内でTypeScript経験に差がある場合、レビュー負荷が一時的に増えることがあります。

  • 「anyで逃げる」運用が増えると、導入メリットが薄れ、型が形骸化します。

  • 学習コストは“払う価値がある負債”になりやすく、長期運用・機能追加が多いプロジェクトほど回収しやすい傾向があります。

導入判断では、プロジェクトの寿命、チームのスキルセット、将来の拡張頻度を踏まえ、「今の開発効率」と「将来の保守性」のどちらを重視するかを明確にすると迷いが減ります。

よくある誤解と「型定義」がもたらす価値

typescript with reactにおいて誤解されやすいのが、「型がある=バグがなくなる」「型さえ付ければ設計が良くなる」といった期待です。実際の型は、あくまで“契約を表現する道具”であり、正しく使うほど価値が出ます。

  • 誤解:型があるから実行時エラーは起きない
    型はコンパイル時の保証であり、外部APIの不正データやネットワーク障害など実行時の問題は別途考慮が必要です。ただし、データの形を型として定義することで「想定外を扱う箇所」が明確になり、対策ポイントが見える化されます。

  • 誤解:とにかく厳格にすれば良い
    厳格さは品質を上げますが、移行直後や試作段階で過度に厳格にすると開発が停滞します。型定義は“守りたい境界(契約)”から優先的に適用するのが効果的です。

  • 価値:型定義は「コミュニケーションコスト」を下げる
    Propsやデータ構造の型定義は、仕様を文章で説明する代わりに、利用者へ正しい使い方を提示します。レビューでは「このPropsは任意か必須か」「nullを許容するか」といった論点が型として表面化し、合意形成が速くなります。

つまり型定義の本質的価値は、単なるエラー検知ではなく、開発チーム全体での理解・変更・合意をスムーズにする“設計のインターフェース”として機能する点にあります。

サードパーティライブラリと型の付き合い方

React開発ではサードパーティライブラリの利用が一般的ですが、TypeScript導入時に品質差が出やすいのもこの領域です。結論としては「型が整備されたライブラリを優先しつつ、足りない部分は現実的に補う」姿勢が重要になります。

  • 型定義の有無・品質を選定基準に入れる
    同等の機能を持つライブラリでも、型定義が充実している方が導入後の開発体験が良く、保守もしやすくなります。型情報が整っているほど、Reactコンポーネントに組み込む際の接続ミスを減らせます。

  • 型が弱い/足りない場合の現実的な落としどころを持つ
    型が不十分な箇所を全面的に作り込むとコストが膨らみます。一時的に型の境界を限定し、影響範囲を小さく抑えた上で補完する方が、プロジェクト全体としては健全です。

  • ライブラリ側の型に引きずられて設計が歪むことがある
    型を合わせること自体が目的化すると、React側のコンポーネント設計が不自然になりがちです。まず自分たちのデータ契約(欲しい形)を明確にし、必要なら変換層を設けるなど、責務を切り分ける意識が重要です。

サードパーティとの連携は、typescript with reactの“効果が出るポイント”でもあり“詰まりやすいポイント”でもあります。型を「品質と速度の両方を上げるための道具」として扱い、過不足なく運用することが成功の鍵です。

開発環境のセットアップ(新規・既存プロジェクト)

react+typescript+vite

typescript with react をスムーズに始めるには、最初に「新規で作るのか」「既存プロジェクトに後から入れるのか」を切り分けて、手順を最短化するのがポイントです。この章では、環境構築でつまずきやすい設定ファイルや依存関係の追加を、目的別に整理して説明します。

新規プロジェクトをTypeScriptで作成する

新規開発なら、最初からTypeScriptテンプレートで作成するのが最も簡単で、設定の取りこぼしが起きにくい方法です。特に現在のReact開発では、プロジェクト作成時点でTypeScriptを選べるツールが一般的です。

代表的な作り方は次のとおりです。

  • Vite:React + TypeScript のテンプレートが用意されており、構成が軽量で立ち上がりが速い
  • Create React App(CRA):従来からある定番。TypeScriptテンプレートがある(ただし新規ではVite採用が増加傾向)
  • Next.js:Reactベースのフレームワーク。初期セットアップでTypeScriptを導入できる

たとえばViteで作成する場合は、次のようにReact + TypeScriptテンプレートを選びます。

npm create vite@latest my-app -- --template react-ts
cd my-app
npm install
npm run dev

作成直後に最低限確認しておくべきファイルは以下です(ツールにより多少差があります)。

  • tsconfig.json:TypeScriptの型チェックやコンパイル方針を定義
  • src配下の.tsx:ReactコンポーネントをTypeScriptで書く拡張子
  • package.jsontypescript@types/...など、型関連依存関係が入っているか

また、エディタはVisual Studio Codeを使うと、TypeScriptの型情報(補完・エラー表示)を活かしやすく、typescript with react の開発体験が安定します。

既存のReactプロジェクトへTypeScriptを段階的に追加する

既存のReact(JavaScript)プロジェクトにTypeScriptを入れる場合は、「一気に全面移行」ではなく、ビルドを壊さない範囲で段階的に共存させるのが現実的です。基本方針は、JSのまま動く状態を維持しつつ、TSへ少しずつ置き換えることです。

導入の流れは次の順番が安全です。

  1. TypeScriptと型定義パッケージを追加
  2. tsconfig.json を用意(または最小構成で生成)
  3. 一部のファイルだけ .ts / .tsx にリネームして移行開始
  4. 型エラーを潰しながら移行範囲を広げる

まず依存関係を追加します(npmの例)。

npm install -D typescript @types/react @types/react-dom

次に、tsconfig.jsonを用意します。既存プロジェクトで段階導入する際は、最初は厳格にしすぎず、移行の進捗に合わせて強化するのがコツです。たとえば「まずはコンパイル可能にする」ことを優先するなら、以下のような設定から始められます。

{
  "compilerOptions": {
    "target": "ES2020",
    "lib": ["ES2020", "DOM", "DOM.Iterable"],
    "module": "ESNext",
    "moduleResolution": "Bundler",
    "jsx": "react-jsx",
    "strict": false,
    "skipLibCheck": true
  },
  "include": ["src"]
}

その上で、Reactコンポーネントから優先的に移行したい場合は、まず App.jsx のようなファイルを App.tsx にリネームします。ここで重要なのは、「JSXを含むなら .tsx」にすることです(.tsだとJSXが書けません)。

段階導入で起きがちな詰まりどころは、次の2点です。

  • ビルド/テスト設定がTypeScriptファイルを解釈しない:バンドラやテストランナー側にTS対応の設定が必要な場合があります
  • 型定義が不足してエラーが出る:React本体とは別に @types/... が必要なケースがあります(特に古い構成)

移行を進める際は、まず「依存関係の追加 → 最小tsconfig → 1ファイルだけTSX化 → 動作確認」という小さなサイクルを回すと、typescript with react の導入がチーム開発でも破綻しにくくなります。

TypeScriptでReactコンポーネントを書く基本

typescript+react+typesafety

「typescript with react」を実務で使い始めると、まず直面するのが“コンポーネントの型付けをどう書くのが定石か”という問題です。TypeScriptは型推論が強力な一方、React特有のProps・children・style・イベントなどには“よく使われる書き方”があります。ここでは、Reactコンポーネントを型安全に書くための基本を、よくある実装シーンに沿って整理します。

関数コンポーネントの型付け(Props / State)

関数コンポーネントでは、まずPropsの型を定義し、引数に付与するのが基本です。Stateはフックで持つことが多いため、初期値から型推論させるか、必要に応じて明示します。Propsを明確にしておくと、呼び出し側のJSXで誤った値を渡した瞬間に検知でき、保守性が上がります。

type UserCardProps = {
  name: string;
  age?: number; // 任意
  onSelect: (name: string) => void;
};

export function UserCard({ name, age, onSelect }: UserCardProps) {
  // Stateは初期値から推論できるなら任せる
  const [selected, setSelected] = React.useState(false);

  return (
    <div>
      <p>{name} {age ? `(${age})` : ""}</p>
      <button
        type="button"
        onClick={() => {
          setSelected(true);
          onSelect(name);
        }}
      >
        Select
      </button>
      {selected && <span>selected</span>}
    </div>
  );
}

Propsの型付けで押さえるポイントは次のとおりです。

  • age?: numberのように任意Propsを明示し、未指定ケースをUI側で扱う
  • イベント的なProps(例:onSelect)は引数・戻り値まで具体的に書く
  • Propsはコンポーネントの“公開API”なので、可能な限り意図が伝わる命名と粒度にする

Stateの型は、多くの場合は初期値から推論されます。ただし、初期値がnullや空配列・空オブジェクトなどだと意図通りに推論されないことがあるため、必要な時だけ明示します。

type User = { id: string; name: string };

export function UserList() {
  // 空配列だけだと never[] 方向に寄るケースを避けたい時は明示
  const [users, setUsers] = React.useState<User[]>([]);

  // nullを取りうるならユニオンで表現
  const [selectedUser, setSelectedUser] = React.useState<User | null>(null);

  return (
    <div>{/* ... */}</div>
  );
}

childrenの扱いと型付けの定石

childrenはReactの重要な拡張ポイントですが、型の付け方を誤ると「文字列はOKにしたいのに弾かれる」「単一要素しか受け取れない」などの不便が起きます。基本の定石は、React.ReactNodeで受けることです。これにより、要素・文字列・配列・nullなど“Reactが描画できるもの”を広く受け入れられます。

type PanelProps = {
  title: string;
  children: React.ReactNode;
};

export function Panel({ title, children }: PanelProps) {
  return (
    <section>
      <h4>{title}</h4>
      <div>{children}</div>
    </section>
  );
}

「必ず単一のReact要素を渡してほしい」など、制約を強めたい場合はReact.ReactElementを使う選択肢もあります。ただし制約が強くなるほど利用側の自由度は下がるため、UI部品の用途に応じて選びます。

type SingleChildWrapperProps = {
  children: React.ReactElement;
};

export function SingleChildWrapper({ children }: SingleChildWrapperProps) {
  return <div className="wrapper">{children}</div>;
}

また、childrenの有無が任意なら、children?: React.ReactNodeとして“ないケース”を型としても許可しておくと、利用側の記述が自然になります。

style propsの型付け(CSSPropertiesなど)

インラインスタイルをPropsとして受け渡す場合は、CSSのキーや値を型で補完できるようにしておくと、typoや不正な値を減らせます。ReactではReact.CSSPropertiesが一般的で、styleの型としてそのまま使えます。

type BoxProps = {
  style?: React.CSSProperties;
  children: React.ReactNode;
};

export function Box({ style, children }: BoxProps) {
  return (
    <div
      style={{
        padding: 12,
        borderRadius: 8,
        ...style,
      }}
    >
      {children}
    </div>
  );
}

定石としては次の考え方が実用的です。

  • styleは任意にし、コンポーネント側のデフォルトとマージできるようにする
  • “許可するスタイルを限定したい”場合は、CSSPropertiesをそのまま渡すのではなく、必要な項目だけをProps化する(例:paddingbgColorなど)
  • 型があることでbackgroudColorのようなスペルミスを早期に検出できる

DOMイベントの型付け(onClick / onChange など)

typescript with reactでイベントを扱うときは、「ブラウザのEvent」ではなく「ReactのSyntheticEvent」を前提とした型を使います。Reactはイベントをラップしているため、型もReact側のものに合わせるのが基本です。特に入力フォームのonChangeは、対象要素(input / textarea / select)によって型が変わるので注意します。

ボタンのクリックなら、React.MouseEventHandler<HTMLButtonElement>のようにHandler型を使うと読みやすくなります。

type ActionButtonProps = {
  onClick: React.MouseEventHandler<HTMLButtonElement>;
  label: string;
};

export function ActionButton({ onClick, label }: ActionButtonProps) {
  return (
    <button type="button" onClick={onClick}>
      {label}
    </button>
  );
}

入力値を扱うonChangeは、React.ChangeEvent<HTMLInputElement>を使うのが定石です。イベントから値を取り出すときはe.currentTarget.valueを使うと、型的にも意図が明確になります。

export function NameField() {
  const [name, setName] = React.useState("");

  const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
    setName(e.currentTarget.value);
  };

  return (
    <label>
      Name
      <input value={name} onChange={handleChange} />
    </label>
  );
}

なお、Propsとしてイベント関数を受け取る場合は「イベントオブジェクトを渡すのか」「値だけ渡すのか」を設計として統一すると、利用側の混乱が減ります。たとえば入力コンポーネントでは、イベントそのものではなく文字列だけを渡す設計もよく使われます。

type TextInputProps = {
  value: string;
  onValueChange: (value: string) => void;
};

export function TextInput({ value, onValueChange }: TextInputProps) {
  return (
    <input
      value={value}
      onChange={(e) => onValueChange(e.currentTarget.value)}
    />
  );
}

実装時に詰まりやすいポイントと対処

TypeScriptでReactコンポーネントを書き始めた直後に詰まりやすいのは、「推論に任せた結果、思ったより厳しい型になった」「JSXで渡した値が合わない」など、“型の境界”が見えづらい点です。以下は頻出のつまずきと、その場で使える対処です。

  • 任意Props(?)なのに利用箇所でエラーが出る

    任意Propsは「渡さなくてよい」だけで、使う側ではundefinedの可能性を考慮する必要があります。表示や計算に使う前に、デフォルト値を設定するか、条件分岐でガードします。

    type PriceProps = { price?: number };
    
    function Price({ price }: PriceProps) {
      const safePrice = price ?? 0;
      return <span>{safePrice}</span>;
    }
  • 空配列・null初期化でStateの型が期待とズレる

    空配列やnullは情報量が少なく、推論が弱くなりがちです。配列の要素型やnullを含むユニオン型を明示して、状態の取りうる範囲を固定します。

    type Item = { id: string };
    
    const [items, setItems] = React.useState<Item[]>([]);
    const [active, setActive] = React.useState<Item | null>(null);
  • childrenの型を絞りすぎて使い勝手が悪くなる

    React.ReactElementは強力ですが、文字列や複数要素を弾きます。汎用的なレイアウト・ラッパー系はReact.ReactNodeを基本にし、制約が必要なコンポーネントだけ絞るのが無難です。

  • イベント型でEventTarget周りが扱いづらい

    e.targetは型が広くなりやすい一方、e.currentTargetは“そのハンドラが付与された要素”として型が安定します。フォームではcurrentTargetを優先すると詰まりにくいです。

    const onChange = (e: React.ChangeEvent<HTMLInputElement>) => {
      console.log(e.currentTarget.value);
    };
  • Propsの関数型が曖昧で、呼び出し側が混乱する

    「イベントを渡すのか、値だけ渡すのか」をコンポーネントごとに統一しないと、利用側で型が合わなくなりがちです。フォーム部品は値を渡す、ボタンはクリックイベントを渡す、などルール化すると設計が安定します。

これらの基本を押さえるだけでも、Reactコンポーネントの実装で発生しやすい型エラーの多くは予防できます。特にProps・children・イベントは“再利用される境界”になりやすいので、型を丁寧に設計することが、typescript with reactの効果を最も実感しやすいポイントです。

フックをTypeScriptで安全に使う実践例

react+typescript+hooks

typescript with react で「型が効いている」と実感しやすいのがフック周りです。Reactフックは状態やロジックをコンポーネントに閉じ込められる一方、型が曖昧だとnull事故や分岐漏れが起きやすくなります。このセクションでは、useState / useReducer / useContext / useMemo / useCallback を、実務で破綻しにくい型設計で使うための具体例をまとめます。

useStateの型推論と明示指定の使い分け

useStateは初期値から型推論されるため、多くのケースで型注釈なしでも十分です。一方で「初期値がnull」「空配列で始めたい」「ユニオン型で状態遷移したい」といった状況では、明示指定しないと型が狭まりすぎたり、逆にany的な扱いになって安全性が落ちます。

まずは推論で問題ないパターンです。初期値が明確なら、TypeScriptが適切に推論してくれます。

import { useState } from "react";

export function Counter() {
  const [count, setCount] = useState(0); // number と推論される
  const [loading, setLoading] = useState(false); // boolean

  return (
    <button onClick={() => setCount((c) => c + 1)}>
      {loading ? "Loading..." : `Count: ${count}`}
    </button>
  );
}

次に、明示指定が必要になりやすい代表例が「nullable状態」です。初期値をnullにすると null に型が寄ってしまうため、意図する型を与えます。

import { useState } from "react";

type User = { id: string; name: string };

export function Profile() {
  const [user, setUser] = useState<User | null>(null);

  // user が null の可能性を型が教えてくれる
  if (!user) return <p>未ログイン</p>;

  return <p>こんにちは、{user.name}さん</p>;
}

また、空配列で始めたいときも要注意です。useState([]) だと never[] になり、後から要素を入れられず詰まることがあります。配列要素の型を明示しましょう。

import { useState } from "react";

type Todo = { id: string; title: string; done: boolean };

export function Todos() {
  const [todos, setTodos] = useState<Todo[]>([]); // ここを明示

  return (
    <ul>
      {todos.map((t) => (
        <li key={t.id}>{t.title}</li>
      ))}
    </ul>
  );
}

まとめると、使い分けの指針は以下です。

  • 初期値が具体的(number/boolean/オブジェクト)なら型推論に任せる
  • null / 空配列 / 空オブジェクトなど「初期値が情報不足」なら明示指定する
  • 状態遷移が複雑なら、ユニオン型などで表現し、意図しない代入を防ぐ

useReducerでのAction/State設計と型付け

状態が増えたり、更新パターンが多くなったりするとuseStateは破綻しがちです。useReducerは「状態(State)」「操作(Action)」「遷移(reducer)」を分離でき、TypeScriptの強み(網羅性チェックやdiscriminated union)が最も活きる領域です。

StateとActionを明確に定義し、Actionは type を判別子にしたユニオン(discriminated union)にします。これによりswitchでの分岐漏れを型で検出できます。

import { useReducer } from "react";

type State = {
  count: number;
  step: number;
};

type Action =
  | { type: "increment" }
  | { type: "decrement" }
  | { type: "setStep"; payload: number }
  | { type: "reset" };

const initialState: State = { count: 0, step: 1 };

function reducer(state: State, action: Action): State {
  switch (action.type) {
    case "increment":
      return { ...state, count: state.count + state.step };
    case "decrement":
      return { ...state, count: state.count - state.step };
    case "setStep":
      return { ...state, step: action.payload };
    case "reset":
      return initialState;
    default: {
      // 分岐漏れをコンパイル時に検出するためのテクニック
      const _exhaustive: never = action;
      return state;
    }
  }
}

export function CounterWithReducer() {
  const [state, dispatch] = useReducer(reducer, initialState);

  return (
    <div>
      <p>count: {state.count} / step: {state.step}</p>
      <button onClick={() => dispatch({ type: "decrement" })}>-</button>
      <button onClick={() => dispatch({ type: "increment" })}>+</button>
      <button onClick={() => dispatch({ type: "reset" })}>reset</button>
    </div>
  );
}

設計のポイントは以下です。

  • Actionは「何をしたいか」を表すイベントとして命名し、必要なデータは payload に寄せる
  • StateはUIに必要な情報を最小限にし、派生値は可能なら別で計算(ただしこのセクションでは派生設計の詳細には踏み込みません)
  • never を使った網羅性チェックで、機能追加時の修正漏れを防ぐ

useContextでのContext設計とnull対策

useContextは依存注入に近い感覚で値を渡せますが、TypeScriptでは「Provider未設定」時に値がnull/undefinedになり得る問題をどう扱うかが重要です。実務では、Contextを「必須(nullを許さない)」として設計し、未設定時は即座に例外を投げて早期に気付けるようにするのが堅実です。

まずContextの型を定義し、createContext<T | undefined>(undefined) の形にしておきます。そして専用フックでガードします。

import { createContext, useContext, useMemo, useState, ReactNode } from "react";

type AuthContextValue = {
  userId: string | null;
  login: (userId: string) => void;
  logout: () => void;
};

const AuthContext = createContext<AuthContextValue | undefined>(undefined);

export function AuthProvider({ children }: { children: ReactNode }) {
  const [userId, setUserId] = useState<string | null>(null);

  const value = useMemo<AuthContextValue>(
    () => ({
      userId,
      login: (id) => setUserId(id),
      logout: () => setUserId(null),
    }),
    [userId]
  );

  return <AuthContext.Provider value={value}>{children}</AuthContext.Provider>;
}

export function useAuth(): AuthContextValue {
  const ctx = useContext(AuthContext);
  if (!ctx) {
    throw new Error("useAuth must be used within AuthProvider");
  }
  return ctx;
}

この形にしておくと、利用側はnullチェックのノイズなく安全にアクセスできます(ただし userId 自体がnullableな設計なら、その分の分岐は必要です)。

import { useAuth } from "./auth";

export function Header() {
  const { userId, logout } = useAuth();

  return (
    <header>
      {userId ? (
        <button onClick={logout}>Logout</button>
      ) : (
        <span>Guest</span>
      )}
    </header>
  );
}

null対策の要点は以下です。

  • Contextのデフォルト値に「ダミーオブジェクト」を入れない(実行時に誤動作しやすい)
  • undefined を初期値にして、専用フックでProvider必須を強制する
  • Contextの値そのものにnullableが含まれる場合は、nullableの意味(未ログイン等)をStateとして明確化する

useMemoでの戻り値型と依存配列の注意点

useMemoは「重い計算結果」や「参照の安定化」を目的に使われますが、typescript with react では戻り値の型が自然に推論される一方、依存配列の指定ミスがバグの温床になりがちです。型安全と同じくらい「依存関係を正しく表現する」ことが重要です。

戻り値型は推論で十分なことが多いものの、API境界(例えば「この関数は必ずこの型を返す」)を明確にしたい場合は明示しておくと読みやすくなります。

import { useMemo } from "react";

type Item = { id: string; price: number; qty: number };
type Summary = { totalQty: number; totalPrice: number };

export function CartSummary({ items }: { items: Item[] }) {
  const summary = useMemo<Summary>(() => {
    return items.reduce<Summary>(
      (acc, cur) => ({
        totalQty: acc.totalQty + cur.qty,
        totalPrice: acc.totalPrice + cur.price * cur.qty,
      }),
      { totalQty: 0, totalPrice: 0 }
    );
  }, [items]);

  return (
    <p>
      qty: {summary.totalQty} / price: {summary.totalPrice}
    </p>
  );
}

依存配列の注意点はシンプルです。コールバック内で参照している値は、原則として依存配列に含めます。含めないと「古い値を掴む」可能性があります。逆に、依存に入れる値が毎回新規参照だとuseMemoが効かないため、上位で参照を安定させる必要が出ることもあります。

  • 依存配列に必要な値が入っていない:更新されるべきタイミングで更新されない
  • 依存配列に毎回変わる参照が入っている:毎回再計算され、効果が薄い
  • 戻り値の型は推論で十分だが、複雑なreduceやオブジェクト構築では型を明示すると保守性が上がる

useCallbackでの関数型・引数/戻り値の型付け

useCallbackは「関数参照を安定させたい」場面で使います。TypeScriptでは関数の引数・戻り値の型が推論されることが多いですが、propsとして渡すハンドラや、非同期処理(Promise)を含む場合は型を明確にしておくと呼び出し側の安全性が上がります。

基本形は、引数の型を付ける(または関数型として定義して使い回す)ことです。

import { useCallback, useState } from "react";

type OnSelect = (id: string) => void;

export function Picker() {
  const [selected, setSelected] = useState<string | null>(null);

  const onSelect = useCallback<OnSelect>((id) => {
    setSelected(id);
  }, []);

  return (
    <div>
      <p>selected: {selected ?? "none"}</p>
      <button onClick={() => onSelect("a")}>A</button>
      <button onClick={() => onSelect("b")}>B</button>
    </div>
  );
}

非同期関数の場合は戻り値が Promise<T> になるため、明示しておくと呼び出し側がawait前提で扱いやすくなります。

import { useCallback, useState } from "react";

type SaveResult = { ok: true } | { ok: false; reason: string };
type SaveFn = (value: string) => Promise<SaveResult>;

export function Editor() {
  const [value, setValue] = useState("");

  const save = useCallback<SaveFn>(async (v) => {
    if (v.trim().length === 0) return { ok: false, reason: "empty" };
    // 実際の保存処理は省略
    return { ok: true };
  }, []);

  return (
    <div>
      <input value={value} onChange={(e) => setValue(e.target.value)} />
      <button
        onClick={async () => {
          const res = await save(value);
          if (!res.ok) alert(res.reason);
        }}
      >
        Save
      </button>
    </div>
  );
}

useCallbackの依存配列もuseMemo同様に重要です。コールバック内で参照するprops/stateを正しく依存に入れることで、型安全だけでなく挙動も一貫します。

  • イベントハンドラをpropsで渡すなら、関数型(例:type OnSelect = (id: string) => void)を定義して統一すると読みやすい
  • 非同期関数は Promise を含めた戻り値型を明示すると、呼び出し側の扱いが安全になる
  • 依存配列は「コールバック内で参照している値」を基準にし、古い値の参照(stale closure)を避ける

便利な型とよく使うテクニック集

react+typescript+generics

typescript with react では、Propsや状態(State)を「正しく設計する」だけでなく、実装を進める中で出てくる“部分的に扱いたい”“必要なものだけ取り出したい”“値の形が状況で変わる”といった現場の要求に、型でスマートに対応できることが大きな強みです。ここでは、React開発で頻出のユーティリティ型・ジェネリクス・型ガードを、実務で使える形に絞って整理します。

ユーティリティ型(Partial / Pick / Omit など)の活用

ユーティリティ型は「既存の型を再利用しながら、用途に合わせて変形する」ための道具です。Reactでは、フォームの一時入力・更新API・表示専用のPropsなど、同じデータモデルを“少しだけ違う形”で扱う場面が多いため、特に効果を発揮します。

ポイントは「新しい型を作り直さない」ことです。ベースとなる型(例:User)を一度定義し、用途に応じてPartial/Pick/Omitで派生させると、変更に強くなります。

  • Partial<T>:すべてのプロパティを任意(オプショナル)にする(フォームの途中状態など)
  • Pick<T, K>:必要なプロパティだけ抜き出す(表示用Props、サマリー表示など)
  • Omit<T, K>:特定のプロパティを除外する(UIで扱わない内部情報を除くなど)
type User = {
  id: string;
  name: string;
  email: string;
  role: "admin" | "member";
};

// フォームの「編集中」状態(全部揃っていなくてもよい)
type UserDraft = Partial<User>;

// 一覧表示など、最小限だけPropsに渡したい
type UserRow = Pick<User, "id" | "name">;

// 画面側では role を変更させない(編集UIから除外)
type UserEditable = Omit<User, "role">;

ReactコンポーネントのProps設計でも活躍します。たとえば「ベースPropsに対して、特定ページでは一部だけ上書き可能にしたい」場合、Omitで衝突を避けつつ拡張できます。

type BaseButtonProps = {
  label: string;
  onClick: () => void;
  variant: "primary" | "secondary";
};

// label を children に寄せたい場合など、衝突するキーを Omit して再定義
type ButtonProps = Omit<BaseButtonProps, "label"> & {
  children: string;
};

function Button({ children, onClick, variant }: ButtonProps) {
  return (
    <button onClick={onClick} data-variant={variant}>
      {children}
    </button>
  );
}

加えて、オブジェクトのキーを型として扱いたいときは keyof と組み合わせると強力です(「存在するキーしか指定できない」状態を作れます)。

type UserKey = keyof User; // "id" | "name" | "email" | "role"

ジェネリクスで再利用性を上げる書き方

ジェネリクス(Generics)は「型を引数として受け取る」仕組みで、汎用ロジックを型安全に再利用するための中心技術です。typescript with react の開発では、UIコンポーネントやカスタムフック、コールバックの型を“データ型に依存せず”使い回したいケースが頻出します。

まず典型は「リスト系コンポーネント」です。T をアイテム型として受け取り、描画関数やキー取得関数を渡す形にすると、どんな配列でも型安全にレンダリングできます。

type ListProps<T> = {
  items: T[];
  getKey: (item: T) => string;
  renderItem: (item: T) => React.ReactNode;
};

function List<T>({ items, getKey, renderItem }: ListProps<T>) {
  return (
    <ul>
      {items.map((item) => (
        <li key={getKey(item)}>{renderItem(item)}</li>
      ))}
    </ul>
  );
}

次に「ユーティリティ関数/フックでの型の伝播」です。ジェネリクスがあると、入力の型から出力の型が自然に決まり、呼び出し側で余計な型注釈が減ります。

// 例:idで検索して見つからない可能性も含めて返す
function findById<T extends { id: string }>(items: T[], id: string): T | undefined {
  return items.find((x) => x.id === id);
}

また、ジェネリクスは「制約(constraints)」を付けるのが実務的です。上の例のように T extends { id: string } としておけば、id を持たない型はコンパイル時に弾けます。これにより、実行時エラーの芽を早期に潰しつつ、コンポーネントや関数の再利用性も保てます。

注意点として、ジェネリクスを過剰に抽象化すると読みづらくなりがちです。複数の型引数が必要になった段階で、呼び出し側の記述量・理解コストと釣り合っているかを意識すると、保守性を落としにくくなります。

型ガードで実行時チェックと型安全性を両立する

TypeScriptの型はコンパイル時のものなので、APIレスポンスや外部入力のような「実行時に形が保証できない値」を扱うときは、型だけでは安全になりません。そこで重要になるのが型ガードです。型ガードは、実行時のチェック結果をTypeScriptに伝え、以降の処理を安全に絞り込む仕組みです。

まず基本の型ガードは typeofininstanceof です。Reactコンポーネントの分岐レンダリングでもよく使います。

type Result =
  | { status: "ok"; data: string }
  | { status: "error"; message: string };

function ResultView({ result }: { result: Result }) {
  if (result.status === "error") {
    return <p>Error: {result.message}</p>;
  }
  return <p>Data: {result.data}</p>;
}

さらに実務では「ユーザー定義の型ガード(type predicate)」が便利です。関数が value is X を返す形にすると、呼び出し側で型が狭まり、nullや不正形状を扱うコードが読みやすくなります。

type User = { id: string; name: string };

function isUser(value: unknown): value is User {
  return (
    typeof value === "object" &&
    value !== null &&
    "id" in value &&
    "name" in value &&
    typeof (value as any).id === "string" &&
    typeof (value as any).name === "string"
  );
}

function UserName({ data }: { data: unknown }) {
  if (!isUser(data)) {
    return <p>Invalid user</p>;
  }
  return <p>{data.name}</p>;
}

型ガードの価値は、「不正な値を弾く(実行時)」と「以降の処理を型安全にする(コンパイル時)」を同時に満たせる点にあります。特にtypescript with react では、レンダリング分岐・イベント入力・外部データ表示が多いため、型ガードを適切に用意しておくと、条件分岐が整理されてバグも減ります。

周辺ツールと一緒に使うときの勘所

react+typescript+tooling

typescript with react を実務で安定運用するには、型だけでなく周辺ツール(Lint/Formatter、ビルド、テスト)との噛み合わせが重要です。ここでは「設定の要点」と「ハマりやすいズレ」を最小限の手戻りで潰すための勘所を整理します。

Lint/FormatterとTypeScriptの基本設定

React×TypeScriptでは、ESLintとPrettierを併用するのが一般的です。ポイントは「ESLintは品質・安全性」「Prettierは整形」に役割を分け、TypeScript用のルールを適切に有効化することです。typescript with react の現場では、型情報を使ったLint(type-aware lint)まで踏み込むかが分かれ道になります。

  • ESLintはTypeScript対応のパーサ/プラグインを入れる
    TypeScript構文を解釈できるようにし、React用ルールと併用します。型に依存しないルールだけでも、未使用変数や危険なanyの混入を抑制できます。

  • Prettierは「整形だけ」に集中させる
    ESLint側で整形系ルールと衝突すると、保存時に差分が揺れやすくなります。基本方針は「整形はPrettier、その他はESLint」で統一すると、PRレビューも楽になります。

  • type-aware lintを使うなら設定とコストを理解する
    型情報を使うルール(例:未使用のPromise、危険な型アサーションの検知など)は強力ですが、tsconfig参照が必要になりLintが重くなりがちです。まずはCIのみでtype-awareを回す、など段階導入が現実的です。

設定の要点としては、ESLint側でTypeScriptを解釈できる状態にし、プロジェクトのtsconfigと整合するようにします。特にモノレポや複数tsconfig構成(アプリ用/テスト用/ビルド用など)の場合、ESLintが参照するtsconfigのズレで誤検知が増えるため注意が必要です。

バンドラ・ビルドツールとの連携ポイント

ビルド周りは「型チェック」と「トランスパイル(JS化)」をどう分担するかが核心です。typescript with react では、ビルドツールが高速化のために型チェックを省略し、別プロセスでtscを走らせる構成も多く見られます。

  • トランスパイルと型チェックを分離するか決める
    高速なビルドを優先する場合、ビルドツールは変換に集中させ、型チェックは tsc --noEmit を別途CI/開発中に実行します。これにより体感速度は上がりますが、「型エラーがビルドを止めない」状態にならないよう運用ルールが必要です。

  • tsconfigの役割を明確にする
    React向けのJSX設定、モジュール解決、パスエイリアス、出力有無などをどのtsconfigで担うかを決めます。ビルドツール側のalias設定とtsconfigの paths が不一致だと、エディタでは通るのにビルドで落ちる、といったズレが起きがちです。

  • 型チェック対象の範囲をコントロールする
    生成物やビルド成果物、型定義生成ディレクトリを誤って含めると、不要な型エラーやLint対象の肥大化につながります。逆に除外しすぎると見逃しが増えるため、対象範囲は「意図を持って」調整します。

  • 環境変数や静的アセットの型を揃える
    環境変数やimportするアセット(画像・SVGなど)を使う場合、型定義(declare)を用意しないとTypeScript側で躓きます。ビルドツールの機能とTypeScriptの型解釈を一致させるのがポイントです。

運用上は「ローカル開発は速く、CIは厳格に」のバランスが取りやすいです。たとえば開発中は高速な変換+必要十分なLint、CIで tsc --noEmit を必須にする、といった形にすると、速度と安全性の両方を確保できます。

テスト環境での型の扱い(型定義・モック)

テストでは「ランタイムの挙動」を検証する一方で、TypeScriptの型とモックが衝突しやすいのが特徴です。typescript with react のテストを安定させるには、型定義の入れ方と、モックを“型安全に書く”ための方針を揃えることが重要です。

  • テストランナー/DOM環境の型定義を揃える
    テスト環境では、テストランナーのグローバル関数(例:describe, it)やDOM APIの型が必要になります。型定義が不足すると、実装は動くのにTypeScriptだけエラーになるため、利用ツールに対応する型定義を明示的に導入します。

  • モックは「本物の型」から作る
    関数やモジュールのモックを作る際、手で型を書き直すと実装と乖離しがちです。可能な限り typeofReturnTypeParameters などで実体から型を引き、変更に強いモックにします。

  • 部分モックは過不足が出やすいので方針を決める
    巨大なオブジェクト(APIレスポンス等)を部分的にモックする場合、必要最低限だけ作ると型が厳しすぎて書きづらくなり、逆に緩めすぎるとテストの信頼性が落ちます。テスト用のファクトリ関数を用意し、既定値+差分指定で生成する形にすると、型とメンテ性のバランスが取りやすくなります。

  • 型アサーション(as)での“握りつぶし”は最小限に
    テストは例外的に as unknown as ... が増えやすい領域ですが、多用すると型の恩恵が消えます。どうしても必要な箇所は理由を明確にし、局所化(ヘルパー関数に閉じ込める等)するのが安全です。

テストの型エラーは「設定不足(型定義がない)」「モックが実装の型とズレた」の2パターンが多いです。前者はtsconfig(テスト用のtsconfigを分けるなど)で整理し、後者は“実体の型からモックを組み立てる”方針に寄せると、typescript with react のテストは破綻しにくくなります。

フォーム実装で学ぶReact×TypeScriptの現場感

react+typescript+form

フォームは「入力値の形が複雑」「バリデーションが絡む」「UI部品が多い」ため、ReactにTypeScriptを組み合わせたときの恩恵(安全性・保守性)が最も体感しやすい領域です。typescript with react の現場では、単に型を付けるだけでなく、バリデーション・フォーム状態・UIコンポーネントの型を“同じ真実”として揃える設計が重要になります。

バリデーションスキーマと型の整合性を取る

フォームでありがちな事故は、「TypeScript上の型は正しいのに、バリデーションルールとズレていて実行時に落ちる」またはその逆です。これを避けるには、スキーマを基準に型を生成(推論)し、単一のソースに寄せるのが定石です。

代表的には Zod や Yup などのスキーマバリデーションを使い、スキーマから入力型を導出します。例えば Zod なら z.infer<typeof schema> でフォーム値の型を作れます。こうすると「必須・任意」「文字数」「列挙値」などのルールが、型にも反映されやすくなります。

import { z } from "zod";

const userSchema = z.object({
  name: z.string().min(1, "必須です"),
  age: z.number().int().min(0),
  role: z.enum(["admin", "member"]),
  tags: z.array(z.string()).default([]),
});

type UserFormValues = z.infer<typeof userSchema>;

整合性を崩しやすいポイントも押さえておくと安全です。

  • HTML入力は基本「文字列」input type="number"でも実際は文字列で渡ってくることが多く、スキーマが z.number() だと不一致になりがちです(変換前提の設計にする)。
  • null と undefined:UI側が未選択を null で持つのか undefined で持つのか、スキーマ・型・フォーム初期値で統一します。
  • 任意フィールドの扱いoptional() の場合でも、フォーム初期値で空文字 "" を入れてしまうと、型は合ってもバリデーションで弾かれることがあります。

結局のところ、typescript with react のフォームでは「入力→(必要なら変換)→スキーマで検証→型として扱う」という流れを決め、スキーマと型の二重管理を避けるのがポイントです。

フォームライブラリでの型付け(入力値・エラー・配列フィールド)

フォームライブラリを使うと、入力値・エラー・送信状態などが整理されますが、TypeScriptで型を通すには「フォーム値の型(Values)」を中心に据えるのが基本です。代表例として React Hook Form はジェネリクスで値の型を渡せるため、typescript with react との相性が良いです。

type Values = {
  email: string;
  password: string;
  skills: { name: string; level: 1 | 2 | 3 }[];
};

const { register, handleSubmit, formState: { errors }, control } =
  useForm<Values>({
    defaultValues: {
      email: "",
      password: "",
      skills: [{ name: "", level: 1 }],
    },
  });

このとき型付けで効いてくるのが、次の3点です。

  • 入力値(Values)の型register("email") のようなキー指定が型安全になり、存在しないフィールド名や型不一致をコンパイル時に検知できます。
  • エラー(errors)の型errors.email?.message のような参照がフィールド構造に追随します。ネストしたオブジェクトや配列でも、誤った参照を防げます。
  • 配列フィールドskills のような繰り返し入力は、追加・削除時にインデックス参照が増えるため、型が崩れると一気に保守性が落ちます。配列要素の型を明確にし、フォームの操作API(例:フィールド配列管理)と整合するように設計します。

また、値の「文字列→数値」変換の扱いは現場で頻出です。例えばレベルを数値として持ちたいなら、入力時に変換する方針(フォーム側で変換する/送信前に変換する)を決め、スキーマ・型・UI入力の整合を取ります。ここが曖昧だと、TypeScript上は number のつもりでも実データは string になり、バリデーションやAPI送信でズレが出ます。

UIコンポーネントライブラリ利用時の型の注意点

UIコンポーネントライブラリ(例:MUIなど)をフォームに組み込むと、「見た目は同じ input でも props の型が独自」「イベントの型が素のDOMと違う」などの理由で、typescript with react の型エラーが出やすくなります。特に注意したいのは、value/onChange の型のズレref の受け渡しです。

  • value の型が広すぎる/狭すぎる:ライブラリ側が string | number のように広く取っている場合、アプリ側の厳密なユニオン型(例:"admin" | "member")と噛み合わないことがあります。フォーム値の型を崩さずに、UI層で変換・マッピングする設計が安全です。
  • onChange のイベント型が異なる:コンポーネントによっては DOM の ChangeEvent<HTMLInputElement> ではなく、独自のシグネチャ(例:(value: T) => void)のことがあります。フォームライブラリの想定する形に合わせ、間にアダプタ関数を置くと破綻しにくいです。
  • ref が通らない問題:フォームライブラリが register 等で ref を要求するのに、UIコンポーネントが ref を内側の input に転送していない(または別propsで渡す)ケースがあります。その場合はライブラリの推奨統合方法(Controller等のラッパ)を使い、ref/値/変更通知を明示的に接続します。

要するに、UIコンポーネントは「見た目の入力」でも、型の入口・出口がDOM入力と一致しないことがあるため、フォーム値の型を中心に据え、UI層でズレを吸収するのが現場的な落としどころです。これを徹底すると、フォームが大きくなっても型が破綻しにくく、改修時の事故も抑えられます。

小さな題材で動かして理解する(TODOアプリ例)

react+typescript+todo

「typescript with react」を効率よく身につけるには、いきなり大規模な画面を作るより、入出力が明確な小さな題材を“動かしながら”理解するのが近道です。ここではTODOアプリを題材に、型を付けるべきポイントを段階的に増やしていきます。最終的には「Props」「状態」「イベント」「コンポーネント分割」に型が行き届いた状態を目指します。

手順1:プロジェクト作成と初期設定

まずはTypeScript前提のReactプロジェクトを作り、型チェックが効く状態を用意します。最初の段階で“TypeScriptとしてコンパイルされる”ことを確認しておくと、以降の手順で出るエラーがすべて「意図した型エラー」になり、学習がスムーズです。

  1. TypeScriptテンプレートでReactプロジェクトを作成します。

    npm create vite@latest todo-ts -- --template react-ts
    cd todo-ts
    npm install
    npm run dev
  2. 拡張子が .tsx になっていることを確認します(JSXを含むTypeScript)。例えば src/App.tsx が対象です。

  3. 最低限の型チェックの挙動を確認します。例として、存在しない変数を参照してエラーになること、VS Code等で型情報が出ることを確認するとよいでしょう。

ここまでで、TODOアプリを“型のあるReact”として育てていく土台が整いました。

手順2:単一TODOコンポーネントを型付きで作る

次に、最小単位として「1件のTODOを表示するコンポーネント」を作ります。ここでの狙いは、データ構造(TODO)とPropsの型をはっきり定義し、表示に必要な情報だけを受け取る設計にすることです。

まずはTODOの型を作ります。どこからでも参照しやすいように src/types.ts のような場所にまとめておくと後の分割が楽です。

// src/types.ts
export type Todo = {
  id: string;
  title: string;
  completed: boolean;
};

続いて単一TODO表示コンポーネントを作ります。Propsは「Todoを1つ受け取る」だけに絞ります。

// src/components/TodoItem.tsx
import type { Todo } from "../types";

type Props = {
  todo: Todo;
};

export function TodoItem({ todo }: Props) {
  return (
    <div>
      <span>{todo.completed ? "✅" : "⬜️"}</span>
      <span>{todo.title}</span>
    </div>
  );
}

ポイントは、todo の形が崩れた場合(例:title を渡し忘れる、completed がboolean以外になる)に、使用側でコンパイル時に検知できることです。これが「typescript with react」の最も基本で強力な恩恵です。

手順3:TODOリストのコンポーネント分割と型設計

次に、複数件を扱う「リスト」へ拡張します。ここでは、Todo[] をPropsとして受け取り、内部で TodoItem を繰り返し描画する構造にします。型設計の観点では「配列の要素型をTodoに固定する」「keyに使うIDがstringである」点が自然に守られます。

// src/components/TodoList.tsx
import type { Todo } from "../types";
import { TodoItem } from "./TodoItem";

type Props = {
  todos: Todo[];
};

export function TodoList({ todos }: Props) {
  return (
    <div>
      {todos.map((todo) => (
        <div key={todo.id}>
          <TodoItem todo={todo} />
        </div>
      ))}
    </div>
  );
}

この段階で「Todoという型をアプリ全体で共有する」構造になり、TODOにフィールドを追加・変更したときの影響範囲が型エラーとして見えるようになります。コンポーネント分割の学習と、型の恩恵が噛み合うポイントです。

手順4:一覧表示とイベント処理の型付け

TODOアプリらしくするために、一覧表示に加えて「追加」「完了切り替え」「削除」などのイベント処理を入れます。ここでは、イベントそのものの型(例:入力のChangeイベント)と、コールバックPropsの型(例:onToggle(id: string))を明確にするのが重要です。

まずはApp側で状態と操作関数を用意し、リスト側へ“型付きの操作”として渡します。

// src/App.tsx
import { useMemo, useState } from "react";
import type { Todo } from "./types";
import { TodoList } from "./components/TodoList";

export default function App() {
  const [todos, setTodos] = useState<Todo[]>([
    { id: "1", title: "TypeScriptで型を付ける", completed: false },
    { id: "2", title: "Reactで表示する", completed: true },
  ]);

  const [title, setTitle] = useState<string>("");

  const remainingCount = useMemo(
    () => todos.filter((t) => !t.completed).length,
    [todos]
  );

  const handleTitleChange: React.ChangeEventHandler<HTMLInputElement> = (e) => {
    setTitle(e.target.value);
  };

  const addTodo = () => {
    const trimmed = title.trim();
    if (!trimmed) return;

    const newTodo: Todo = {
      id: crypto.randomUUID(),
      title: trimmed,
      completed: false,
    };
    setTodos((prev) => [newTodo, ...prev]);
    setTitle("");
  };

  const toggleTodo = (id: string) => {
    setTodos((prev) =>
      prev.map((t) => (t.id === id ? { ...t, completed: !t.completed } : t))
    );
  };

  const deleteTodo = (id: string) => {
    setTodos((prev) => prev.filter((t) => t.id !== id));
  };

  return (
    <div>
      <h1>TODO</h1>
      <p>未完了: {remainingCount}</p>

      <div>
        <input value={title} onChange={handleTitleChange} />
        <button onClick={addTodo}>追加</button>
      </div>

      <TodoList todos={todos} onToggle={toggleTodo} onDelete={deleteTodo} />
    </div>
  );
}

それに合わせて TodoList を「表示 + 操作の受け口」に拡張し、コールバックの型を定義します。

// src/components/TodoList.tsx
import type { Todo } from "../types";
import { TodoItem } from "./TodoItem";

type Props = {
  todos: Todo[];
  onToggle: (id: string) => void;
  onDelete: (id: string) => void;
};

export function TodoList({ todos, onToggle, onDelete }: Props) {
  return (
    <div>
      {todos.map((todo) => (
        <TodoItem
          key={todo.id}
          todo={todo}
          onToggle={onToggle}
          onDelete={onDelete}
        />
      ))}
    </div>
  );
}

最後に TodoItem 側でも「どんな操作ができるコンポーネントか」を型で表現します。ここまで来ると、イベント処理の取り回しが“文字列のidで統一されている”ことが型で保証され、実装の迷いが減ります。

// src/components/TodoItem.tsx
import type { Todo } from "../types";

type Props = {
  todo: Todo;
  onToggle: (id: string) => void;
  onDelete: (id: string) => void;
};

export function TodoItem({ todo, onToggle, onDelete }: Props) {
  return (
    <div>
      <label>
        <input
          type="checkbox"
          checked={todo.completed}
          onChange={() => onToggle(todo.id)}
        />
        {todo.title}
      </label>

      <button onClick={() => onDelete(todo.id)}>削除</button>
    </div>
  );
}

手順5:動作確認とよくある型エラーの潰し込み

最後に、アプリが動くことを確認しつつ、初心者が遭遇しやすい型エラーを“意図して潰せる”状態にします。typescript with reactでは、エラーを消すこと自体よりも「なぜその型が必要だったか」を理解するのが重要です。

  • イベント型が合わないonChange の引数を (e) => とだけ書いていると、状況によって型が曖昧になりやすいです。今回のように React.ChangeEventHandler<HTMLInputElement> を使うと、e.target.value へのアクセスが安全になります。

  • useStateの初期値から意図しない型になる:空配列で useState([]) を始めると never[] 扱いになって詰まることがあります。useState<Todo[]>([]) のように要素型を明示すると解決します。

  • コールバックPropsの引数型が不一致onToggle(id: string) => void と決めたのに、呼び出し側で number を渡すなどのミスは、型定義があることで即座に検知できます。IDの型をアプリ全体で統一するのがコツです。

  • Todo型の更新漏れTodo にフィールドを追加した際、newTodo の生成や表示側で不足があると型エラーになります。これは面倒ではなく、更新箇所を洗い出す“チェックリスト”として働きます。

このTODOアプリの一連の流れを通すと、Reactの部品設計に合わせてTypeScriptの型が自然に育ち、実務で頻出の「Props」「状態」「イベント」「コールバック」の基本形が一通りつかめます。

さらに学ぶためのロードマップ

react+typescript+nextjs

typescript with react を一通り触った後は、「Reactの設計力」と「TypeScriptの型設計力」を別々に鍛えつつ、実務に近い題材で統合していくと伸びが速くなります。ここでは、次に学ぶと効果が高いテーマをロードマップとして整理します。

Reactの理解を深めるための学習テーマ

React×TypeScriptの生産性は、型付けだけで決まるわけではありません。コンポーネント設計や状態の持ち方が不安定だと、型を付けても変更に弱い実装になりがちです。まずはReactの「なぜそう書くのか」を説明できる状態を目指しましょう。

  • レンダリングの仕組み(再レンダリングの条件):どの状態変更がどのコンポーネントに影響するか、props/state/contextが更新をどう伝播させるかを理解します。無駄な再レンダリングを避ける設計の判断軸が持てます。

  • コンポーネント分割と責務設計:Container/Presentationalのような役割分担、UIとロジックの分離、再利用可能な部品化の基準(どこまで汎用化するか)を整理します。

  • 状態管理の選択肢と使い分け:ローカルstate、context、外部ストアの「どれをいつ選ぶか」を言語化します。状態のスコープ、更新頻度、依存関係の複雑さから判断できるようにします。

  • 副作用の扱い(データ取得・購読・クリーンアップ):UIと非同期処理の接続、ローディング/エラー状態の表現、購読解除などのライフサイクル相当の考え方を身につけます。

  • パフォーマンスの基礎:メモ化の前に「ボトルネックを特定する」姿勢を持ち、必要な箇所に限定して最適化できるようにします。

TypeScriptの理解を深めるための学習テーマ

typescript with react では、PropsやAPIレスポンス、フォーム値など「形が変わりやすいデータ」をどう表現するかが重要です。型を“付ける”から一段進み、型を“設計する”学習に移ると、変更に強い実装になります。

  • Union/Intersectionと判別可能なUnion:画面状態(loading / error / success)やモード切替などを、if分岐で安全に絞り込める形で表現できるようにします。

  • Generics(ジェネリクス)の実戦投入:汎用コンポーネント(リスト、テーブル、セレクト等)で「型を受け取って型を返す」設計を学び、再利用性と型安全性を両立します。

  • Utility Typesの使い分けPick/Omit/Partial/Requiredなどで、同じドメイン型から用途別の型を派生させる練習をします(重複定義を減らす発想)。

  • 型の境界設計(unknown→安全な型へ):外部入力(API、localStorage、フォーム入力)はまずunknownとして受け、実行時チェックで安全に型を確定する流れを学びます。型だけで安全だと誤解しないための重要ポイントです。

  • 型エラーの読み解きと最小修正:エラー文の「期待される型」「実際の型」「不足しているプロパティ」を素早く特定し、型のねじれを根本から直す習慣を付けます。

次に取り組むと効果が高い実践課題(Next.jsなど)

理解を定着させるには、実装量よりも「現実の制約」を入れることが近道です。Next.jsのようなフレームワークや、実務に寄せた課題で、設計・型・運用をまとめて経験すると、typescript with react のスキルが一段上がります。

  1. Next.jsで「ページ+データ取得+型」の一連を作る:一覧→詳細の2画面を作り、APIレスポンス型・UI表示モデル・エラー状態の型を分けて設計します。UI都合の型に引っ張られない練習になります。

  2. 認証がある前提の画面遷移:ログイン/未ログインで表示が変わるケースを、判別可能なUnionで状態管理します。条件分岐の散らかりを型で整理できるようになります。

  3. フォーム中心の機能(登録・編集・確認)を作る:入力値の型、送信Payloadの型、表示用の整形後データの型を分離します。「同じデータでも目的で型が変わる」感覚が掴めます。

  4. 汎用コンポーネントを1つ作って運用する:例としてテーブル/モーダル/セレクトなどをジェネリクスで型安全にし、複数画面で使い回します。変更要求が来たときに、型が安全にリファクタを導く体験が得られます。

  5. 小さな設計レビューを自分に課す:実装後に「型の責務は適切か」「anyで逃げていないか」「境界でunknownを扱えているか」「状態の表現はUnionで整理できるか」をチェックし、次の改善点を言語化します。