【実務・中級編】 TypeScriptにおけるuseStateの型推論とジェネリクス – React実践ガイド

やあ。今日も元気にコンポーネントをリファクタリングしているかい?
フロントエンドの現場にいると、TypeScriptとReactの組み合わせはもはや「息をするのと同じ」くらい当たり前になったよね。

でも、ちょっと待ってほしい。君が何気なく書いているその `useState`、本当に型安全と言い切れるかい?
「とりあえず `useState(null)` にしておけば怒られないし大丈夫っしょ!」なんて妥協して、後から `null` チェックの嵐に溺れたり、型推論に甘えて `any` の亜種を生み出したりしていないかい?

今日は、中級から一歩抜け出して「真のフロントエンド・プロフェッショナル」を目指す君に向けて、TypeScriptにおける `useState` の型推論とジェネリクス、そして現場で絶対に押さえておくべき実践的な知見を授けよう。

—

なぜ `useState` の型定義でつまずくのか?

Reactの `useState` は、一見すると非常にシンプルなAPIだ。しかし、TypeScriptと組み合わせた途端、私たちの意図を華麗に裏切ってくる瞬間がある。

例えば、こんなコードを見たことはないだろうか?

// よくある、でも少しもったいない書き方
const [user, setUser] = useState(null);

TypeScriptの型推論は優秀だ。この場合、初期値が `null` であるため、`user` の型は `null`(厳密には `null` 型)に固定されてしまう。後から `setUser({ id: 1, name: ‘Taro’ })` と呼ぼうものなら、TypeScriptのコンパイラから「そんな型はねぇ!」と赤色の波線で怒られてしまうわけだ。

ここに、私たちが明示的にジェネリクス(``)を使って型を教えてあげる必要性がある。

—

1. ジェネリクス(``)の基本と、型推論の限界を見極める

まずは基本に立ち返ろう。`useState` は次のようなシグネチャを持つジェネリック関数だ。

function useState(initialState: S | (() => S)): [S, Dispatch>];

初期値から型を推論してくれるケース(プリミティブ型など)では、わざわざジェネリクスを書く必要はない。

// 推論に任せてOKな例
const [isOpen, setIsOpen] = useState(false); // boolean型と正しく推論される
const [count, setCount] = useState(0); // number型と正しく推論される

しかし、問題は「最初はデータがない(あるいは空である)が、後から特定のオブジェクトや配列が入ってくる」という、実務で100回は遭遇するユースケースだ。

実務で頻出:初期値が `null` や `undefined` になり得るパターン

APIからユーザーデータを取得するコンポーネントを想像してほしい。マウント時はまだデータがないため `null` だが、フェッチが成功すれば `User` オブジェクトが入る。

ここでジェネリクスの出番だ。

import { useState, useEffect } from ‘react’;

// ユーザーの型定義
interface User {
id: number;
name: string;
email: string;
}

export const UserProfile = () => {
// ジェネリクスを使って「Userか、あるいはnullである」と明示する
const [user, setUser] = useState(null);

useEffect(() => {
// 適当なAPIフェッチの模倣
fetchUser().then((data) => {
setUser(data); // ここで User 型を渡しても怒られない!
});
}, []);

if (!user) {
return

読み込み中…

;
}

return (

{user.name}

{user.email}

);
};

この書き方であれば、`user` が `null` の可能性があることをTypeScriptがコンパイル時に強制的に意識させてくれるため、`user.name` にアクセスする前に必ずガード(`if (!user)` など)を入れる習慣が自然と身につく。これが「型安全」の恩恵というやつさ。

—

2. フォームや入力値の管理における「型アサーション」の誘惑と罠

さて、次はもう少し踏み込んだ話をしよう。
複数の入力フィールドを持つフォームの状態管理などで、初期値は空っぽだけど、型としては特定のオブジェクト構造を強制したい場合がある。

ここで、やってはいけないアンチパターンを一つ紹介しよう。

// 【アンチパターン】型アサーション(as)の乱用
interface FormValues {
username: string;
age: number;
}

// 気持ちは分かるが、これはコンパイラを無理やり黙らせているだけで危険
const [form, setForm] = useState({} as FormValues);

`as FormValues` を使うと、TypeScriptは「あ、こいつがそう言うなら、この空のオブジェクト `{}` は `FormValues` 型に違いない」と勘違いしてくれる。
しかし、この状態のまま `form.username.toUpperCase()` なんて呼び出そうものなら、Runtime(実行時)に `TypeError: Cannot read properties of undefined` が爆誕し、白昼メンテンスの悪夢を見る羽目になる。

ベストプラクティス:初期値の設計を見直す、または部分的な型を許容する

もし初期値が完全に揃わないのであれば、型定義側でオプショナル(`?`)を使うか、あるいは「入力途中」の型を別に定義するのがプロの作法だ。

interface FormValues {
username: string;
age: number | ”; // 未入力時は空文字を許容するなどの工夫
}

const [form, setForm] = useState({
username: ”,
age: ”, // 初期値から型を満たすように設計する
});

どうだい?これなら無理な型アサーションを使わなくても、型推論と初期値が美しく一致する。コードの意図が読み手にもスッと伝わるはずだ。

—

3. 配列のステートにおける型定義の落とし穴

もう一つ、現場でよくあるのが「配列のステート」だ。
todoリストやアイテム一覧を保持するとき、初期値に空配列 `[]` を指定すると、TypeScriptは親切心(?)からこれを `never[]`(何も要素を持つことが許されない配列)と推論してしまうことがある。

// 初期値が空配列だと、型が never[] に推論されてしまうことがある
const [todos, setTodos] = useState([]);

// 後からオブジェクトを追加しようとするとコンパイルエラーになる!
setTodos([{ id: 1, text: ‘TypeScriptを極める’ }]);

これを防ぐためには、もちろんジェネリクスを使う。

interface Todo {
id: number;
text: string;
completed: boolean;
}

// 正しいアプローチ:ジェネリクスで Todo の配列であることを明示する
const [todos, setTodos] = useState([]);

たったこれだけの違いだが、これを知っているか知らないかで、コードの堅牢性と開発スピードが劇的に変わってくる。

—

ブラウザの裏側とReactの最適化の視点

ここで少し、Reactが内部でどう動いているかという「裏側の話」をしておこう。
Reactの `useState` は、内部的にはFiberと呼ばれるツリー構造の中で、コンポーネントごとの「メモリスロット(linked list)」に状態を順番に保存している。

TypeScriptの型というのは、あくまで「開発時のコンパイルタイムに私たちのうっかりミスを防ぐためのガードレール」であり、ブラウザが実行するJavaScript(バンドルされたコード)の時点では、型情報は綺麗さっぱり消え去っている(Type Erasure)。

だからこそ、型定義をサボって `any` や無理なアサーション(`as`)で型エラーをハックしてしまうと、TypeScriptという最強のセーフティネットを自分からドブに捨てることになる。
「ブラウザは型を見てくれないが、TypeScriptは未来の自分のバグを防いでくれる」——この意識をチーム全体で共有できるかどうかが、プロダクトの寿命を左右するんだ。

—

まとめ

今日覚えておいてほしいポイントは以下の3つだ。

1. プリミティブな型は推論に任せる: `boolean` や `number` などは無理にジェネリクスを書かず、推論の力を信じる。
2. `null` や `undefined` が入り得るなら明示する: `useState(null)` のように、状態のライフサイクル(未ロード、ロード済み)を型で表現する。
3. 空の配列やオブジェクトで `never[]` やアサーション(`as`)の罠にハマらない: 初期値の設計を見直すか、適切なジェネリクスを付与して型安全性を保つ。

型定義は、単なる「TypeScriptのコンパイラを黙らせるための儀式」じゃない。
「未来の自分や、一緒に働くチームメイトに向けた、最高に親切なドキュメント」なんだよ。

さあ、今日の業務に戻って、自社のリポジトリにある `useState` たちを誇り高くリファクタリングしてやろうじゃないか。
何か質問があれば、いつでも私のデスクまで聞きに来てくれ。プロとしての議論を歓迎するよ。

コメント

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