TypeScript:型ガードと絞り込み
1学習の目的
- typeof・in・タグ付きユニオンで、ユニオン型を安全に絞り込めるようになる。TS06以降で何度も使ってきた「if で確認すると型が確定する」仕組みを、体系的に理解する。
- 型ガード関数を自作できるようになる。「この値はこの型だ」と検証する処理を、型システムに教え込めるようになる。
2基礎解説
TS06から「if で確認すると、その中では絞り込まれた型として扱える」という現象を使ってきました。これを型の絞り込み(Narrowing)と呼びます。絞り込む手段はいくつもあります。
| 手段 | 書き方 | 向いている対象 |
|---|---|---|
| typeof | typeof v === "number" | 基本型(number/string等) |
| in | "bark" in animal | プロパティの有無で判別 |
| タグ付きユニオン | r.kind === "success" | 実務で最も多い |
| 型ガード関数 | function isX(v): v is X | 自作の判定ロジック |
- 絞り込みは if の中だけで有効。if (typeof v === "number") の内側では number として扱えるが、外に出ると再びユニオン型に戻る。
- in はプロパティの有無で型を判別する。オブジェクトの形が違うユニオン型に使える。
- タグ付きユニオンが最も安全。kind のような共通の目印を持たせ、その値で判別する。実務のAPIレスポンスやReduxのアクションはこの形が定番。
- 型ガード関数の戻り値は v is X と書く。単に boolean と書くと絞り込みが効かない——この違いが重要。
- Array.isArray() や === null でも絞り込める。TypeScriptは普通のJavaScriptの条件文を理解して、型を追跡してくれる。
現場使用例:APIの成功/失敗レスポンスの判別、フォーム入力の検証、複数の形式を受け付ける関数、外部データのフィルタリング。
// typeof で絞り込む
function describe(v: number | string): string {
if (typeof v === "number") {
return `数値:${v.toFixed(1)}`; // ここでは number
}
return `文字列:${v.length}`; // ここでは string
}
// in でプロパティの有無から判別
type Dog = { bark: () => string };
type Cat = { meow: () => string };
function speak(animal: Dog | Cat): string {
if ("bark" in animal) {
return animal.bark(); // ここでは Dog
}
return animal.meow(); // ここでは Cat
}
// タグ付きユニオン(実務で最も多い形)
type Result =
| { kind: "success"; data: string }
| { kind: "failure"; error: string };
function handle(r: Result): string {
if (r.kind === "success") {
return `成功:${r.data}`;
}
return `失敗:${r.error}`;
}
// 型ガード関数(v is X が肝)
function isString(v: unknown): v is string {
return typeof v === "string";
}
const values: unknown[] = [1, "a", true, "b"];
const strings = values.filter(isString); // string[] になる
console.log(strings); // ['a', 'b']
3基本ドリル(10問)
出力2行。3.1 / 3
typeof で分岐する。number なら toFixed(1)、string なら length。
function describe(v: number | string): string {
if (typeof v === "number") {
return v.toFixed(1);
}
return String(v.length);
}
console.log(describe(3.14));
console.log(describe("abc"));
if の中では number として扱えるので toFixed が使える。絞り込まれていなければ「文字列にはtoFixedが無い」とエラーになっていた。
A. number のまま B. 元のユニオン型に戻る C. any になる D. エラーになる 選択★☆☆無料
解答 == B
絞り込みが有効な範囲はどこまでか。
B
絞り込みは if のブロック内だけで有効。外に出れば元の型に戻るので、使う場所ごとに確認が必要になる。
出力2行。ワン / ニャー
if ("bark" in animal) と書くと、そのブロック内では Dog として扱える。
type Dog = { bark: () => string };
type Cat = { meow: () => string };
function speak(animal: Dog | Cat): string {
if ("bark" in animal) {
return animal.bark();
}
return animal.meow();
}
console.log(speak({ bark: () => "ワン" }));
console.log(speak({ meow: () => "ニャー" }));
in はプロパティの有無で型を判別する。オブジェクトの形が異なるユニオン型に有効な手段。
function f(v: number | string): number {
if (____ v === "number") {
return v * 2;
}
return v.length;
}
console.log(f(21));
出力 == 42
値の型を調べる演算子。小文字6文字。
typeof
typeof は基本型(number・string・boolean など)の判定に使う。オブジェクトの種類は判別できないので、その場合は in やタグを使う。
A. boolean B. v is string C. string D. is string 選択★☆☆無料
解答 == B
「v はこの型である」と型システムに教える書き方。
B
単に boolean と書くと絞り込みが効かない。v is string と書くことで「true を返したら v は string だ」とTypeScriptに伝えられる。
出力2行。成功:取得完了 / 失敗:通信エラー
r.kind === "success" で判別する。それぞれのブロックで data / error が使える。
type Result =
| { kind: "success"; data: string }
| { kind: "failure"; error: string };
function handle(r: Result): string {
if (r.kind === "success") {
return `成功:${r.data}`;
}
return `失敗:${r.error}`;
}
console.log(handle({ kind: "success", data: "取得完了" }));
console.log(handle({ kind: "failure", error: "通信エラー" }));
これがタグ付きユニオン。kind という共通の目印で判別すると、それぞれのブロックで固有のプロパティが安全に使える。
function isNumber(v: unknown): v ____ number {
return typeof v === "number";
}
const vals: unknown[] = [1, "a", 2];
console.log(vals.filter(isNumber).length);
出力 == 2
「〜である」を意味する2文字のキーワード。
is
v is number と書くと、filter の結果が number[] になる。単なる boolean では unknown[] のままで、絞り込みが効かない。
A. typeof で判定 B. 共通のタグ(kind など)で判別 C. try/catch で試す D. any にする 選択★★☆無料
解答 == B
typeof では全て object としか分からない。
B
typeof はオブジェクトの種類を区別できない(どれも "object" になる)。共通のタグを持たせるのが最も明確で安全な方法。
出力 == a, b
function isString(v: unknown): v is string を定義し、filter に渡す。
function isString(v: unknown): v is string {
return typeof v === "string";
}
const values: unknown[] = [1, "a", true, "b", 2];
const strings = values.filter(isString);
console.log(strings.join(", "));
filter の結果が string[] になるので join が使える。型ガード関数を使わなければ unknown[] のままで、join も呼べなかった。
出力2行。5 / 6
Array.isArray() で判別できる。TypeScriptはこれを型ガードとして理解する。
function total(v: number | number[]): number {
if (Array.isArray(v)) {
return v.reduce((sum, n) => sum + n, 0);
}
return v;
}
console.log(total(5));
console.log(total([1, 2, 3]));
Array.isArray は標準の型ガード。TypeScriptがこの関数を理解しているので、if の中では number[] として扱える。
4実践シナリオ(5問)
「✅ ユーザー取得成功」
「❌ エラー:権限がありません」
「✅ 商品取得成功」 コーディング★★☆無料
出力3行が完全一致
kind で判別し、それぞれ絵文字を付けて出力する。配列で3件を回す。
type ApiResult =
| { kind: "success"; message: string }
| { kind: "error"; message: string };
const results: ApiResult[] = [
{ kind: "success", message: "ユーザー取得成功" },
{ kind: "error", message: "権限がありません" },
{ kind: "success", message: "商品取得成功" },
];
for (const r of results) {
if (r.kind === "success") {
console.log(`✅ ${r.message}`);
continue;
}
console.log(`❌ エラー:${r.message}`);
}
APIの結果を扱う定番の形。成功と失敗で持つデータが違っても、タグで判別すれば安全に処理できる。
出力 == 600
数値ならそのまま、文字列なら変換して NaN でないかを確認する。型ガード関数で数値化できるものだけを残す。
type Input = number | string | null;
function toNumber(v: Input): number | null {
if (typeof v === "number") {
return v;
}
if (typeof v === "string") {
const n = Number(v);
return Number.isNaN(n) ? null : n;
}
return null;
}
function isNumber(v: number | null): v is number {
return v !== null;
}
const inputs: Input[] = [100, "200", null, "abc", 300];
const total = inputs.map(toNumber).filter(isNumber).reduce((s, n) => s + n, 0);
console.log(total);
変換と検証を分けているのが要点。toNumber が「変換できないものは null」に統一し、isNumber が null を除去する。段階を分けると各処理が単純になる。
出力2行。円:314.0 / 長方形:20.0
kind で判別すると、それぞれ radius / width・height が安全に使える。
type Shape =
| { kind: "circle"; radius: number }
| { kind: "rectangle"; width: number; height: number };
function area(s: Shape): number {
if (s.kind === "circle") {
return s.radius ** 2 * 3.14;
}
return s.width * s.height;
}
const shapes: Shape[] = [
{ kind: "circle", radius: 10 },
{ kind: "rectangle", width: 5, height: 4 },
];
for (const s of shapes) {
const label = s.kind === "circle" ? "円" : "長方形";
console.log(`${label}:${area(s).toFixed(1)}`);
}
形が違うデータを1つの型で扱える。円に width は無く、長方形に radius は無い——タグで判別することで、存在しないプロパティへのアクセスが起こりえない。
データ:[{name:"PC",price:1000}, "文字列", {name:"Mouse"}, {name:"Key",price:500}, null]
抽出した商品の合計金額を出力すること。 コーディング★★★無料
出力 == 1500
型ガード関数の中で、オブジェクトであること・name が文字列であること・price が数値であることを順に確認する。
type Product = { name: string; price: number };
function isProduct(v: unknown): v is Product {
if (typeof v !== "object" || v === null) {
return false;
}
const o = v as Record<string, unknown>;
return typeof o.name === "string" && typeof o.price === "number";
}
const data: unknown[] = [
{ name: "PC", price: 1000 },
"文字列",
{ name: "Mouse" },
{ name: "Key", price: 500 },
null,
];
const products = data.filter(isProduct);
const total = products.reduce((sum, p) => sum + p.price, 0);
console.log(total);
外部データの検証はこの形が基本。null も typeof では "object" になるので、明示的に除外する必要がある——JavaScriptの古い仕様に由来する有名な罠。
「注文受付:2026-01-05」
「発送済:追跡番号 ABC123」
「配達完了:2026-01-10」 コーディング★★★無料
出力3行が完全一致
3つの状態それぞれが違うプロパティを持つ。kind で判別して分岐する。
type OrderState =
| { kind: "pending"; orderedAt: string }
| { kind: "shipped"; trackingNo: string }
| { kind: "delivered"; deliveredAt: string };
const states: OrderState[] = [
{ kind: "pending", orderedAt: "2026-01-05" },
{ kind: "shipped", trackingNo: "ABC123" },
{ kind: "delivered", deliveredAt: "2026-01-10" },
];
for (const s of states) {
if (s.kind === "pending") {
console.log(`注文受付:${s.orderedAt}`);
continue;
}
if (s.kind === "shipped") {
console.log(`発送済:追跡番号 ${s.trackingNo}`);
continue;
}
console.log(`配達完了:${s.deliveredAt}`);
}
状態ごとに必要なデータが違うのが現実的な設計。未発送なのに追跡番号を持つ、といった矛盾した状態を型として作れなくなる。
5仕上げ課題
型の定義
・ValidationResult:タグ付きユニオン
- { kind: "valid"; value: string }
- { kind: "invalid"; field: string; reason: string }
検証関数 validate(field: string, input: unknown): ValidationResult
① 文字列でなければ → invalid(理由:「文字列ではありません」)
② 空文字(前後の空白を除く)なら → invalid(理由:「未入力です」)
③ 20文字を超えるなら → invalid(理由:「長すぎます」)
④ それ以外は valid(前後の空白を除いた値)
検証するデータ(4件)
・("名前", "田中太郎") ・("年齢", 30) ・("住所", " ") ・("備考", "あ"を21回)
出力仕様:各結果を1行ずつ出力し、最後に集計を出す
期待される出力(5行)
✅ 田中太郎
❌ 年齢:文字列ではありません
❌ 住所:未入力です
❌ 備考:長すぎます
有効1件 / 無効3件
出力5行が完全一致
validate の中で typeof による絞り込みを行う。unknown を受け取るので、まず文字列かどうかを確認しないと trim() も呼べない。結果はタグ付きユニオンで返し、呼び出し側で kind により分岐する。21文字は "あ".repeat(21)。
type ValidationResult =
| { kind: "valid"; value: string }
| { kind: "invalid"; field: string; reason: string };
function validate(field: string, input: unknown): ValidationResult {
if (typeof input !== "string") {
return { kind: "invalid", field, reason: "文字列ではありません" };
}
const trimmed = input.trim();
if (trimmed.length === 0) {
return { kind: "invalid", field, reason: "未入力です" };
}
if (trimmed.length > 20) {
return { kind: "invalid", field, reason: "長すぎます" };
}
return { kind: "valid", value: trimmed };
}
const inputs: { field: string; value: unknown }[] = [
{ field: "名前", value: "田中太郎" },
{ field: "年齢", value: 30 },
{ field: "住所", value: " " },
{ field: "備考", value: "あ".repeat(21) },
];
let validCount = 0;
let invalidCount = 0;
for (const input of inputs) {
const result = validate(input.field, input.value);
if (result.kind === "valid") {
validCount++;
console.log(`✅ ${result.value}`);
continue;
}
invalidCount++;
console.log(`❌ ${result.field}:${result.reason}`);
}
console.log(`有効${validCount}件 / 無効${invalidCount}件`);
この課題には、型ガードの価値が凝縮されている。
まず validate の引数が unknown であることに注目してほしい。フォームから何が届くか分からない状況を正直に表している。typeof input !== "string" で弾かないと、input.trim() すら呼べない——TypeScriptが検証を強制している。
次に戻り値のタグ付きユニオンだ。valid なら value を、invalid なら field と reason を持つ。「成功したのにエラー理由がある」「失敗したのに値がある」という矛盾した状態を、型として作れない。
そして呼び出し側では result.kind === "valid" で判別するだけで、それぞれのブロックで正しいプロパティにアクセスできる。if を書き忘れて result.value にアクセスしようとすれば、その場でエラーになる。
JavaScriptなら { ok: true, value: ..., error: undefined } のようなオブジェクトを返し、result.error が undefined かどうかを毎回確認していた。型で状態を表現すると、確認漏れが構造的に起こらなくなる。
次章ではジェネリクスを学ぶ。「どんな型でも扱える関数」を、型安全なまま書けるようになる。