2026年10月8日にTypeScript HandbookとZodの本文を再確認した。フロントエンドではコンポーネントのprops、状態、URL、フォーム、API応答、永続化データを別々の境界として型付けし、unknownを検証してから利用する。型はUIの表示や分岐を読みやすくするが、サーバー権限、ネットワークの真実、ユーザー入力、アクセシビリティを代替しない。共有型を増やす前に、変更頻度と契約の所有者を決める。
TypeScriptをフロントエンドで活かす:部品・状態・外部データの型設計
TypeScriptを使っているのに、エラーが出るたびにasやanyを足している。APIから届いた値で画面が止まる。この状態では、型を書く量よりも、どの境界で何を確認するかを見直すほうが先です。
ここではJavaScriptの関数とオブジェクトを読める制作担当者に向けて、部品の入力、画面の状態、外部データの三つから型を整えます。TypeScriptはコード上の不整合を見つける道具であり、外から届く値の検証は別に用意します。
30秒で要点
- 部品へ渡す値と操作を分け、呼び出す側との約束を型にする。
- 読み込み中・成功・失敗を、あり得る組み合わせだけで表す。
- 型推論に任せられる場所へ注釈を重ねず、境界の意図を明示する。
- APIや保存済みデータは
unknownで受け、利用前に値を確認する。 - 型検査が実際に実行される経路と、操作を確かめるテストを持つ。
表が収まらない場合は、横にスクロールできます
| 用語 | この記事での意味 |
|---|---|
| 静的な型検査 | 実行前に、コードで使う値の形や組み合わせを調べること |
| 型推論 | 注釈がなくても、初期値や使い方から型を求めること |
| props / state | 部品へ渡す入力 / 画面が保持する状態 |
| API | プログラム間でデータをやり取りする窓口 |
型が働く場所と、値を調べる場所を分ける
TypeScriptの型注釈は、JavaScriptとして実行するときには残りません。型を指定しただけで、通信相手のJSONに必要な項目があるかを調べる処理が追加されるわけではありません。
例えば、value as Userは「この値をUserとして扱う」という型検査への指示です。数値を文字列へ変換したり、足りない名前を補ったりする処理ではありません。TypeScript公式の型アサーションの説明も、実行時検査を伴わないことを明記しています。
この違いを押さえると、型で表すべき問題と、実行して確かめる問題を分けられます。
表が収まらない場合は、横にスクロールできます
| 起きた問題 | 主に確認する場所 |
|---|---|
| 名前が必要な部品へ、IDしか渡していない | 部品の入力型と呼び出し元 |
| 読み込み中なのに成功データを表示しようとした | 状態の型と分岐 |
| APIが数値の代わりに文字列を返した | 受信時の検証 |
| 正しい型の金額で、計算式だけが間違っている | 計算のテスト |
| チェック項目をキーボードで操作できない | HTMLの要素と操作確認 |
部品へ渡す「データ」と「操作」を分ける
タスクを表示する部品を例にします。タスク自体が持つid・text・completedと、画面から渡す「完了状態を変える関数」は役割が違います。保存するデータへ操作関数を混ぜる必要はありません。
次の.tsx例はReactとその型定義が利用できるプロジェクト向けです。先頭の"use client"はクライアント側で動く部品の宣言で、Next.js App Routerの構成でもこの操作部品を使うために付けています。既存画面へ導入するときは、そのフレームワークの配置規約に従ってください。
"use client";
import { useState } from "react";
export type Todo = {
id: number;
text: string;
completed: boolean;
};
type TodoItemProps = {
todo: Todo;
onToggle: (id: Todo["id"]) => void;
};
export function toggleTodo(todos: Todo[], id: Todo["id"]): Todo[] {
return todos.map((todo) =>
todo.id === id ? { ...todo, completed: !todo.completed } : todo,
);
}
function TodoItem({ todo, onToggle }: TodoItemProps) {
return (
<li>
<label>
<input
type="checkbox"
checked={todo.completed}
onChange={() => onToggle(todo.id)}
/>
{todo.text}
</label>
</li>
);
}
export default function TodoApp() {
const [todos, setTodos] = useState<Todo[]>([
{ id: 1, text: "公開前にリンクを確認する", completed: false },
]);
function handleToggle(id: Todo["id"]) {
setTodos((current) => toggleTodo(current, id));
}
return (
<ul>
{todos.map((todo) => (
<TodoItem key={todo.id} todo={todo} onToggle={handleToggle} />
))}
</ul>
);
}
Todo["id"]は、Todoのidと同じ型を参照しています。useState<Todo[]>は、状態がタスクの配列であることを明示します。currentやmapの中のtodoは文脈から推論されるため、注釈を繰り返していません。propsとstateの基本はReact公式ガイドで確認できます。
クリックできるだけのliを作る代わりに、ラベル付きのチェックボックスを使いました。ただし、このHTMLを選んだことと、実画面でフォーカスや操作を確認したことは別です。ここでの保存先は画面の状態だけで、再読み込みすると初期値に戻ります。
画面の状態は、あり得る組み合わせで表す
loading: boolean、data?: Todo[]、error?: stringを独立にすると、「読み込み中なのにデータとエラーが両方ある」といった状態も作れます。仕様上それを許さない画面なら、状態ごとに持つ値を分けます。
type Todo = {
id: number;
text: string;
completed: boolean;
};
export type TodoState =
| { status: "loading" }
| { status: "success"; data: Todo[] }
| { status: "error"; message: string };
export function stateLabel(state: TodoState): string {
switch (state.status) {
case "loading":
return "読み込み中です";
case "success":
return `${state.data.length}件のタスク`;
case "error":
return state.message;
}
}
これは判別可能なunion型と呼ばれます。statusで場合を分けると、その場合に存在する値へ型が絞られます。成功時だけdataへ触れる関係を、コードに表せます。TypeScript公式の絞り込みの説明
再取得中も前の結果を残す画面なら、この三状態だけでは足りません。その場合は「再取得中で前のデータを持つ」という状態を追加します。型へ合わせて画面の要件を消すのではなく、画面で許す状態を先に決めます。
外部データには、実行時の確認を置く
APIの返答やブラウザに保存した古いデータは、今のコードが期待する形とは限りません。unknownは「まだ形を確認していない値」として扱う型です。次は一件のタスクを調べる小さな例です。
export type Todo = {
id: number;
text: string;
completed: boolean;
};
export function isTodo(value: unknown): value is Todo {
return (
typeof value === "object" &&
value !== null &&
"id" in value && typeof value.id === "number" &&
"text" in value && typeof value.text === "string" &&
"completed" in value && typeof value.completed === "boolean"
);
}
export function readTodo(value: unknown): Todo {
if (!isTodo(value)) {
throw new Error("タスクの形式が想定と異なります");
}
return value;
}
この関数は三項目の型だけを調べます。IDが正の整数か、文字数が収まるか、余分な項目を許すかは検査していません。value is Todoと書くだけで、検査内容の正しさをTypeScriptが証明するわけでもありません。
項目や条件が増える場合は、検証ルールを一か所に書けるライブラリも候補になります。受信値の検証とエラー応答をつなぐ例はTypeScriptとZodによるAPI入力検証で扱います。
型を組み合わせても、実データは変わらない
既存の型から必要な形を作ると、同じ定義を複数箇所で書かずに済みます。
type Todo = {
id: number;
text: string;
completed: boolean;
};
type TodoDraft = Pick<Todo, "text">;
type TodoPatch = Partial<Pick<Todo, "text" | "completed">>;
type TodoWithoutId = Omit<Todo, "id">;
type AssignedTodo = Todo & { assigneeId: string };
Pickは選んだ項目、Omitは除いた残り、Partialは各項目を任意にした型です。&は両方の条件を満たす型を作ります。詳しい用途はユーティリティ型の公式説明にあります。
ただし、Omitを書いても実際のオブジェクトからidは消えません。送信する項目を減らすなら、{ text: todo.text }のように送信データを作る処理が必要です。また、このTodoPatchは空のオブジェクトも許します。「変更項目が一つ以上必要」という条件は、別に検証します。
フレームワークでは型検査の実行経路も確認する
ファイルの拡張子を変えたことと、公開前に型検査が動くことは同じではありません。開発サーバーが動いていても、変換だけを行っている構成があります。
表が収まらない場合は、横にスクロールできます
| 環境 | 確認すること | 公式の入口 |
|---|---|---|
| React | TSX、props、Hooksの型定義と、プロジェクトの型検査コマンド | ReactとTypeScript |
| Vue | 単一ファイル部品の検査。Viteによる変換と型検査を分け、vue-tsc等を実行 | VueのTypeScriptガイド |
| Angular | TypeScript側の設定と、テンプレート側のstrictTemplates等の検査範囲 | テンプレート型検査 |
新しい検査範囲ではstrictを基本に検討します。これは複数の厳密な型検査をまとめて有効にする設定です。既存のtsconfig.jsonの対象ファイルや継承関係も確認し、小さな設定例で全体を置き換えないでください。strictの公式説明
JavaScriptが多い既存システムでは、入力にJavaScriptも含めるallowJsなどを使いながら、変更の多い部品やデータの境界から移す方法があります。導入方法は公式の移行ガイドと、利用しているフレームワークの設定を合わせて確認します。
一つの部品で、検査が役立つか試す
最初に変更する部品を一つ選び、次の順で確認します。
- 入力、状態、操作関数を分けて型を書く。推論されるローカル変数はそのままにする。
- 必須項目を一つ欠かすなど、意図的な不整合が型検査で検出されるか確かめる。
- 外部入力に
null、項目不足、型違いを渡し、利用前に拒否されるか調べる。 - 正常な操作、失敗時の表示、キーボード操作を実際に試す。
- 継続的なチェックに同じ型検査コマンドを入れ、検出範囲を記録する。
評価するのは型注釈の数ではなく、以前見逃していた不整合がどこで分かるようになったかです。開発全体の組み立てと合わせて考える場合はモダンWeb開発ガイドも参照してください。