この課題のねらい
ここは「なんとなく動く 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 />
TSXTypeScript は「name が足りない」と怒ってくれます。
また、こう書いた場合もエラーになります。
<Greeting name={123} />
TSXname は string 型だと定義しているので、number を渡すと型エラーです。
これが「型で守られている」状態です。
state に型定義をつける
useState の型を意識する
次は state です。
TypeScript なしだと、こんな感じで書きがちです。
const [count, setCount] = useState(0);
TSXこれはこれで正しくて、初期値が 0 なので、TypeScript は count を number と推論してくれます。
ただ、初期値が null や空配列のときは、明示的に型をつけたほうが分かりやすくなります。
例えば、TODO の配列を state に持つ場合。
type Todo = {
id: number;
title: string;
completed: boolean;
};
const [todos, setTodos] = useState<Todo[]>([]);
TSXuseState<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 の型は「User か null」です。
なので、使うときは必ず「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>;
}
TSXtype と interface は似ていますが、ざっくり言うと、
- どちらも「オブジェクトの形」を表すのに使える
- 小さなコンポーネントの 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、どんな値が入る前提なんだっけ?」
みたいな迷いが減って、エディタの補完とエラーが「相棒」になってきます。
