TypeScript / LESSON 09 / TS09

TypeScript:型ガードと絞り込み

1学習の目的

2基礎解説

TS06から「if で確認すると、その中では絞り込まれた型として扱える」という現象を使ってきました。これを型の絞り込み(Narrowing)と呼びます。絞り込む手段はいくつもあります。

手段書き方向いている対象
typeoftypeof v === "number"基本型(number/string等)
in"bark" in animalプロパティの有無で判別
タグ付きユニオンr.kind === "success"実務で最も多い
型ガード関数function isX(v): v is X自作の判定ロジック
✅ 覚えるべき重要ポイント
  1. 絞り込みは if の中だけで有効if (typeof v === "number") の内側では number として扱えるが、外に出ると再びユニオン型に戻る。
  2. in はプロパティの有無で型を判別する。オブジェクトの形が違うユニオン型に使える。
  3. タグ付きユニオンが最も安全kind のような共通の目印を持たせ、その値で判別する。実務のAPIレスポンスやReduxのアクションはこの形が定番。
  4. 型ガード関数の戻り値は v is X と書く。単に boolean と書くと絞り込みが効かない——この違いが重要
  5. 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問)

TS09-D01 数値または文字列を受け取り、数値なら小数第1位まで、文字列なら文字数を返す関数 describe を定義せよ。describe(3.14) と describe("abc") の結果を2行出力すること。 コーディング★☆☆無料
期待される結果

出力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が無い」とエラーになっていた。

TS09-D02 if (typeof v === "number") { … }外側では、v の型はどうなるか。
A. number のまま B. 元のユニオン型に戻る C. any になる D. エラーになる
選択★☆☆無料
期待される結果

解答 == B

ヒント

絞り込みが有効な範囲はどこまでか。

模範解答
B
解説

絞り込みは if のブロック内だけで有効。外に出れば元の型に戻るので、使う場所ごとに確認が必要になる。

TS09-D03 { bark: () => string }{ meow: () => string } のユニオン型を受け取り、in 演算子で判別して鳴き声を返す関数 speak を定義せよ。犬と猫の両方で試し、2行出力すること。 コーディング★☆☆無料
期待される結果

出力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 はプロパティの有無で型を判別する。オブジェクトの形が異なるユニオン型に有効な手段。

TS09-D04 型を判定する演算子を補ってコードを完成させよ。 穴埋め★☆☆無料
コード
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 やタグを使う。

TS09-D05 型ガード関数の戻り値の型として正しいのはどれか(引数 v を string と判定する場合)。
A. boolean B. v is string C. string D. is string
選択★☆☆無料
期待される結果

解答 == B

ヒント

「v はこの型である」と型システムに教える書き方。

模範解答
B
解説

単に boolean と書くと絞り込みが効かないv is string と書くことで「true を返したら v は string だ」とTypeScriptに伝えられる。

TS09-D06 タグ付きユニオン Result({ kind: "success"; data: string } または { kind: "failure"; error: string })を定義し、成功なら「成功:取得完了」、失敗なら「失敗:通信エラー」を返す関数 handle を作れ。両方で試し2行出力すること。 コーディング★★☆無料
期待される結果

出力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 という共通の目印で判別すると、それぞれのブロックで固有のプロパティが安全に使える。

TS09-D07 型ガード関数の戻り値の型を補ってコードを完成させよ。 穴埋め★★☆無料
コード
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[] のままで、絞り込みが効かない。

TS09-D08 オブジェクトのユニオン型を判別する方法として、実務で最も推奨されるのはどれか。
A. typeof で判定 B. 共通のタグ(kind など)で判別 C. try/catch で試す D. any にする
選択★★☆無料
期待される結果

解答 == B

ヒント

typeof では全て object としか分からない。

模範解答
B
解説

typeof はオブジェクトの種類を区別できない(どれも "object" になる)。共通のタグを持たせるのが最も明確で安全な方法。

TS09-D09 unknown の配列 [1, "a", true, "b", 2] から、文字列だけを型ガード関数で抽出し、カンマ区切りで出力せよ。 コーディング★★☆無料
期待される結果

出力 == 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 も呼べなかった。

TS09-D10 数値または数値配列を受け取り、配列なら合計、単体ならそのまま返す関数 total を定義せよ。total(5) と total([1,2,3]) の結果を2行出力すること。 コーディング★★☆無料
期待される結果

出力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問)

TS09-S01 【API レスポンスの処理】タグ付きユニオンで ApiResult 型(success: データあり / error: メッセージあり)を定義し、3件の結果を処理して次の3行を出力せよ。
「✅ ユーザー取得成功」
「❌ エラー:権限がありません」
「✅ 商品取得成功」
コーディング★★☆無料
期待される結果

出力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の結果を扱う定番の形。成功と失敗で持つデータが違っても、タグで判別すれば安全に処理できる。

TS09-S02 【入力値の正規化】数値・文字列・null が混在する配列 [100, "200", null, "abc", 300] から、数値として扱える値だけを型ガード関数で抽出し、合計を出力せよ。 コーディング★★☆無料
期待される結果

出力 == 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 を除去する。段階を分けると各処理が単純になる。

TS09-S03 【図形の面積計算】タグ付きユニオンで Shape 型(circle: 半径 / rectangle: 幅と高さ)を定義し、面積を返す関数 area を作れ。円(半径10)と長方形(幅5・高さ4)で試し、「円:314.0」「長方形:20.0」の形式で2行出力すること(円周率3.14、小数第1位)。 コーディング★★☆無料
期待される結果

出力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 は無い——タグで判別することで、存在しないプロパティへのアクセスが起こりえない。

TS09-S04 【安全なデータ抽出】unknown の配列から、商品オブジェクトらしきものだけを型ガード関数で抽出せよ。商品は name(文字列)と price(数値)を持つものとする。
データ:[{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);
解説

外部データの検証はこの形が基本nulltypeof では "object" になるので、明示的に除外する必要がある——JavaScriptの古い仕様に由来する有名な罠。

TS09-S05 【注文状態の処理】タグ付きユニオンで OrderState 型を定義せよ(pending: 注文日 / shipped: 追跡番号 / delivered: 配達日)。3件の注文について次の3行を出力すること。
「注文受付: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仕上げ課題

TS09-FINAL 【フォーム検証システム】ユーザー入力を検証し、結果を集計せよ。

型の定義
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 なら fieldreason を持つ。「成功したのにエラー理由がある」「失敗したのに値がある」という矛盾した状態を、型として作れない

そして呼び出し側では result.kind === "valid" で判別するだけで、それぞれのブロックで正しいプロパティにアクセスできる。if を書き忘れて result.value にアクセスしようとすれば、その場でエラーになる。

JavaScriptなら { ok: true, value: ..., error: undefined } のようなオブジェクトを返し、result.error が undefined かどうかを毎回確認していた。型で状態を表現すると、確認漏れが構造的に起こらなくなる。

次章ではジェネリクスを学ぶ。「どんな型でも扱える関数」を、型安全なまま書けるようになる。