この記事の目次
読み込み中なのに成功件数も出る、成功したはずなのにデータがない。複数のフラグを個別に更新する画面では、想定していない組み合わせを作れてしまいます。TypeScriptの判別可能なユニオンを使い、状態名と、その状態に必要な値を一組にしてみましょう。
JavaScriptのオブジェクト、関数、TypeScriptの型注釈が読める人向けです。架空の音源資料一覧を題材に、画面へ渡す状態と案内文を作ります。通信やDOM操作は含めず、型検査で拒否される例と、実行時に出る五つの文を別々に確認します。
フラグの組み合わせから、許可する状態の一覧へ
loading、error、itemsをそれぞれ独立した項目にすると、loadingがtrueでerrorもあり、itemsにも以前の結果がある、といった値を表せます。その組み合わせが必要な画面もありますが、今回の仕様では一度に一つの状態だけを表示します。
| 状態名 | 必須の情報 | 表示する内容 |
|---|---|---|
| idle | なし | 読み込みを始める前の案内 |
| loading | なし | 結果待ちの案内 |
| success | 資料名の配列 | 件数、または正常な0件の案内 |
| error | 説明文 | 読み込めなかった案内 |
「successならitemsが必要」という関係を型にも書きます。全項目を任意にした一つの型では、statusをsuccessへ変えてもitemsが必須になりません。状態ごとの型を作り、縦棒の記号で「このうちのどれか」として結ぶのがユニオンです。
TypeScript公式の絞り込みの説明では、共通の項目に異なる文字列リテラルを持たせる形を判別可能なユニオンとして扱います。今回の判別項目はstatusです。項目名は自分で決められますが、各状態で同じ名前を使います。
状態を受け取り、必ず一つの案内文を返す
Node.js 24系とnpmを用意し、新しい練習フォルダで次を実行します。ここでは再現用にTypeScript 5.9.3へ固定し、最新版であるとは扱いません。
npm init -y
npm install --save-dev --save-exact typescript@5.9.3
次をstate.tsへ保存します。配列をreadonlyにしているのは、この関数から資料名を追加・削除する必要がないためです。サーバーからデータを取る代わりに、五つの完成した状態を直接渡します。
type State =
| { status: "idle" }
| { status: "loading" }
| { status: "success"; items: readonly string[] }
| { status: "error"; message: string };
function messageFor(state: State): string {
switch (state.status) {
case "idle":
return "読み込みを開始してください。";
case "loading":
return "資料を読み込んでいます。";
case "success":
return state.items.length === 0
? "登録された資料はありません。"
: `${state.items.length}件の資料があります。`;
case "error":
return state.message;
default: {
const unhandled: never = state;
throw new Error(`未処理の状態: ${JSON.stringify(unhandled)}`);
}
}
}
const examples: State[] = [
{ status: "idle" },
{ status: "loading" },
{ status: "success", items: ["鳥の声", "駅の音"] },
{ status: "success", items: [] },
{ status: "error", message: "資料を読み込めませんでした。" }
];
for (const state of examples) console.log(messageFor(state));
型検査と変換を行い、成功した場合にだけ生成したJavaScriptを実行します。空の練習フォルダで作ったpackage.jsonは、ここではtypeをmoduleへ変更しません。
npx tsc state.ts --strict --target ES2022 --module commonjs --noEmitOnError --outDir out
node out/state.js
出力は順に「読み込みを開始してください。」「資料を読み込んでいます。」「2件の資料があります。」「登録された資料はありません。」「資料を読み込めませんでした。」です。successの中で0件を扱うため、取得前や失敗時の案内と混ざりません。
itemsを読む場所が、型の絞り込みを示す
switchでstatusがsuccessと分かると、その分岐ではitemsが必須の型へ絞られます。itemsがあるかを毎回調べたり、未定義かもしれない値へ非nullアサーションの感嘆符を付けたりする必要がありません。
反対にswitchの前では、stateは四種類のどれかです。そこでitemsを読みたいという要求は、loadingでも読むのか、errorでも読むのかという仕様の問いになります。型エラーを消すより、データを使う分岐が正しいかを確認してください。
取得失敗と0件を分ける記事は、通信結果を調べて状態を作る処理です。今回はその後に受け渡す型と、分岐の不足を検出する仕組みが対象です。案内文を考える作業は空の一覧の文言と組み合わせられます。
不足したデータと、増やした状態を別々に試す
一つ目の失敗は、成功へ切り替える際にitemsを渡し忘れることです。state.tsの末尾へ一時的に次を足すと、型検査では必須項目がないというエラーになります。実行前に不足を見つけるための実験なので、検証後はこの行を取り除きます。
const missing: State = { status: "success" };
修正はitemsを任意にすることではなく、結果が0件ならitems: []を渡すことです。まだ結果を得ていないならloadingを選びます。型検査が通った後、0件の案内になるか、結果待ちの案内になるかを別に照合します。
二つ目は、Stateへ| { status: "cancelled" }を加えたのに、表示処理を増やさない失敗です。neverは、到達する値が残っていない場所を型として確認するために使っています。cancelledが未処理ならdefaultに残り、neverへ代入できないというエラーになります。
修正ではswitchへcase "cancelled": return "読み込みを取り消しました。";を追加します。型検査をやり直し、examplesにもcancelledの一件を足して六行目を確認します。defaultで何でも「エラーです」と返すだけでは、状態を追加した際の表示漏れを検出できません。
この型では、外部データと遷移順序は検査しない
この型はアプリ内部で作る状態の契約です。外部JSONをas Stateで受け取っても、実際の形を確認したことにはなりません。入口の値を調べる方法はunknownによる受信データの検査で扱っています。
また、idleから直接successへ進めるべきか、古い応答を採用してよいかという時間上の規則は、この型だけでは強制しません。複数の通信の順序を管理する処理や、状態遷移を検査する処理は別に必要です。neverの検査を、すべての動作の保証へ広げないでください。
前の結果を残しながら再取得する仕様なら、今回のloadingには情報が足りません。データを持つrefreshingなど、必要な状態を追加して表示方法を決めます。一方、単に開閉するだけの部品なら真偽値一つで足りることもあり、必ずユニオンへ変える必要はありません。
拒否される入力と、表示できる状態を照合する
15〜20分を目安に、完成例の実行、不足したsuccess、cancelledの追加を順番に試します。正しい五状態は型検査を通り五行が出る、不足したsuccessは変換前に拒否される、cancelledは分岐を追加してから通る、という三条件が解答です。
最後に二件の配列を一件へ変え、「1件の資料があります。」になることも確かめます。型検査が通った事実と、案内文が期待どおりだった事実を別々に記録してください。JavaScriptの無料レッスンで関数を作る際にも、入力の種類と返す表示をこのように対応させられます。
TRY A LESSON
記事の内容を、課題で確かめる
記載内容へのご質問や訂正のご連絡は、hello@junkbranding.comまでお願いします。