Next.jsで学ぶReact講座(完全初心者向け・30日)演習問題・課題 | 第5章:仕上げ – TypeScript

React Next.js
スポンサーリンク

この課題のねらい

ここは「なんとなく動く React」から「型で守られた React」にステップアップするところです。
TypeScript をちゃんと使えるようになると、

  • props の渡し間違い
  • state の型ミス
  • 「この値って何が入るんだっけ?」という不安

を、かなりの割合でコンパイル時に潰せます。
今回はその中でも、いちばん基礎になる「props と state の型定義」と「interface の使いどころ」を押さえていきます。


props に型定義をつける

「このコンポーネントは何を受け取るのか」を明文化する

まずは、シンプルな Greeting コンポーネントを例にします。
TypeScript を使わないと、こんな感じで書けてしまいます。

function Greeting(props) {
  return <p>こんにちは、{props.name}さん</p>;
}
TSX

一見動きますが、「props に何が入るのか」がコードから読み取りづらいし、
間違った使い方をしてもコンパイル時には分かりません。

ここに型をつけると、こうなります。

type GreetingProps = {
  name: string;
};

function Greeting({ name }: GreetingProps) {
  return <p>こんにちは、{name}さん</p>;
}
TSX

ポイントは 2 つです。

GreetingProps という「props の形」を表す型を定義している。
function Greeting({ name }: GreetingProps) のように、引数にその型をつけている。

これで、

  • name は必須
  • 型は string

ということが、コンパイラにも人間にもはっきり伝わります。

間違った使い方をするとどうなるか

例えば、呼び出し側でこう書いたとします。

<Greeting />
TSX

TypeScript は「name が足りない」と怒ってくれます。
また、こう書いた場合もエラーになります。

<Greeting name={123} />
TSX

namestring 型だと定義しているので、number を渡すと型エラーです。
これが「型で守られている」状態です。


state に型定義をつける

useState の型を意識する

次は state です。
TypeScript なしだと、こんな感じで書きがちです。

const [count, setCount] = useState(0);
TSX

これはこれで正しくて、初期値が 0 なので、TypeScript は countnumber と推論してくれます。
ただ、初期値が null や空配列のときは、明示的に型をつけたほうが分かりやすくなります。

例えば、TODO の配列を state に持つ場合。

type Todo = {
  id: number;
  title: string;
  completed: boolean;
};

const [todos, setTodos] = useState<Todo[]>([]);
TSX

useState<Todo[]> と書くことで、「これは Todo 型の配列ですよ」と宣言しています。
これにより、

  • todos.push("文字列") のような変な操作は型エラーになる
  • todos.map(todo => todo.completed) のように、todo の中身に型補完が効く

というメリットが出てきます。

null を許容する場合の型

API からのデータ取得などで、「最初はまだデータがない」という状態を表したいとき、
null を使うことがよくあります。

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

const [user, setUser] = useState<User | null>(null);
TSX

この場合、user の型は「Usernull」です。
なので、使うときは必ず「null かもしれない」を意識する必要があります。

if (!user) {
  return <p>読み込み中...</p>;
}

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

こういう「型で状態を表現する」感覚がついてくると、
「このタイミングでこの値は絶対に存在する/しない」がコードから読み取りやすくなります。


interface を使った型定義

type と interface の違いをざっくり理解する

props の型を定義するとき、さっきは type を使いましたが、interface でも書けます。

interface GreetingProps {
  name: string;
}

function Greeting({ name }: GreetingProps) {
  return <p>こんにちは、{name}さん</p>;
}
TSX

typeinterface は似ていますが、ざっくり言うと、

  • どちらも「オブジェクトの形」を表すのに使える
  • 小さなコンポーネントの props なら、どっちを使ってもほぼ困らない
  • interface は「あとから拡張できる」という特徴がある

くらいを押さえておけば、最初は十分です。

interface を使うと気持ちいい場面

例えば、TODO アプリで Todo 型を定義するとします。

interface Todo {
  id: number;
  title: string;
  completed: boolean;
}
TSX

これをいろんな場所で使い回せます。

const [todos, setTodos] = useState<Todo[]>([]);

type TodoItemProps = {
  todo: Todo;
};
TSX

もし後から「期限(dueDate)を追加したい」となったとき、
interface Todo にフィールドを足すだけで、Todo を使っているすべての場所に反映されます。

interface Todo {
  id: number;
  title: string;
  completed: boolean;
  dueDate?: string; // 追加(? は「なくてもいい」オプション)
}
TSX

これで、

  • todo.dueDate を使おうとすると、「string | undefined」として扱われる
  • つまり「期限がない場合もある」ということが型に表現される

という状態になります。

props に interface を使うパターン

コンポーネントの props も、interface で書くと見通しがよくなります。

interface TodoItemProps {
  todo: Todo;
  onToggle: (id: number) => void;
  onDelete: (id: number) => void;
}

function TodoItem({ todo, onToggle, onDelete }: TodoItemProps) {
  return (
    <div>
      <input
        type="checkbox"
        checked={todo.completed}
        onChange={() => onToggle(todo.id)}
      />
      <span>{todo.title}</span>
      <button onClick={() => onDelete(todo.id)}>削除</button>
    </div>
  );
}
TSX

ここで大事なのは、

  • 「このコンポーネントは何を受け取るのか」が一目で分かる
  • 間違った props を渡そうとすると、コンパイル時に止めてくれる

という点です。


まとめ:TypeScript でつかんでほしい感覚

この課題で押さえてほしいのは、次の感覚です。

props には「このコンポーネントは何を受け取るのか」を型で明文化する。
state には「この値は何型なのか」「null を取り得るのか」を意識して型をつける。
オブジェクトの形(Todo, User, Props など)は、type でも interface でも定義できるが、interface でまとめておくと拡張しやすい。

ここが分かってくると、

「このコンポーネント、何を渡せばいいんだっけ?」
「この state、どんな値が入る前提なんだっけ?」

みたいな迷いが減って、エディタの補完とエラーが「相棒」になってきます。

タイトルとURLをコピーしました