08. 型の絞り込み

union を安全に使う / この資料の山場②

📅 作成: 2026-08-29 / 更新: 2026-08-29

この章の到達点

この章の内容

  1. typeof と instanceof
  2. in と真偽値による絞り込み
  3. 判別可能ユニオン
  4. 型ガード関数
  5. never による網羅性チェック

08.1 typeof と instanceof

if を書くと型が変わる

TypeScript は条件分岐の中で型を絞り込みます。これを制御フロー解析と呼びます。

function show(id: string | number) {
	// ここでは string | number

	if (typeof id === "string") {
		console.log(id.toUpperCase());    // ここでは string
	} else {
		console.log(id.toFixed(2));       // ここでは number
	}
}

これは TypeScript 特有の考え方です。

Java・C# では、instanceof で確認してもキャストを書かなければその型として使えません(Java 16 以降のパターンマッチングを除く)。TypeScript では確認しただけで型が変わります。キャストは不要です。

typeof で判定できるもの

絞り込まれる型注意
typeof x === "string"string
typeof x === "number"numberNaN も number
typeof x === "boolean"boolean
typeof x === "function"関数型
typeof x === "object"オブジェクト型 + nullnull も "object" になる。別途 x !== null が要る
typeof x === "undefined"undefined

typeof null === "object" は JavaScript の歴史的な仕様です。

オブジェクトかどうかを判定するときは、必ず null のチェックを併記してください。

if (typeof x === "object" && x !== null) { /* ... */ }

instanceof — クラスの判定

class ApiError extends Error {
	constructor(public status: number, message: string) {
		super(message);
	}
}

function handle(e: unknown) {
	if (e instanceof ApiError) {
		console.log(e.status);       // ApiError として扱える
	} else if (e instanceof Error) {
		console.log(e.message);      // Error として扱える
	}
}

instanceof はクラスにしか使えません。

typeinterface実行時に存在しないため、x instanceof User と書くことはできません(01. 背景)。オブジェクトの形を判定したいときは in か型ガード関数を使います。

08.2 in と真偽値による絞り込み

in — プロパティの有無で判定する

type Dog = { bark: () => void };
type Cat = { meow: () => void };

function speak(animal: Dog | Cat) {
	if ("bark" in animal) {
		animal.bark();      // Dog
	} else {
		animal.meow();      // Cat
	}
}

真偽値による絞り込み

function show(name: string | undefined) {
	if (name) {
		console.log(name.toUpperCase());   // string(undefined が除かれた)
	}
}

空文字と 0 に注意してください。

if (name)undefined だけでなく空文字 "" も偽として扱います。数値なら 0 も偽です。「値が存在するか」を判定したいなら、明示的に書いてください。

if (name !== undefined) { /* "" も通る */ }
if (name != null)       { /* null と undefined だけを除く */ }

!= は通常避けますが、x != nullnullundefined の両方を一度に除く慣用句として使われます。

絞り込みが消える場面

絞り込みは再代入やコールバックをまたぐと失われます。

function f(v: string | undefined) {
	if (!v) return;
	// ここでは string

	[1, 2].forEach(() => {
		console.log(v.toUpperCase());   // OK(v は const 相当)
	});
}

function g(obj: { v?: string }) {
	if (!obj.v) return;
	// ここでは obj.v は string

	setTimeout(() => {
		console.log(obj.v.toUpperCase());
		//              ~
		// error TS18048: 'obj.v' is possibly 'undefined'.
		// (間に obj.v が変わる可能性があるため)
	});
}

プロパティは変わりうるため、絞り込みが保持されません。ローカル変数に取り出すと解決します。

function g(obj: { v?: string }) {
	const v = obj.v;
	if (!v) return;
	setTimeout(() => console.log(v.toUpperCase()));   // OK
}

08.3 判別可能ユニオン

実務で最もよく使う設計

共通のプロパティに、それぞれ別のリテラル型を持たせます。そのプロパティを見れば、どれなのか判定できます。

type Result =
	| { status: "success"; data: string }
	| { status: "error"; message: string }
	| { status: "loading" };

function show(r: Result) {
	switch (r.status) {
		case "success":
			console.log(r.data);       // data がある
			break;
		case "error":
			console.log(r.message);    // message がある
			break;
		case "loading":
			console.log("読み込み中");
			break;
	}
}
r.status を見る 判別のためのプロパティ "success" status: "success" data: string "error" status: "error" message: string "loading" status: "loading" (他のプロパティなし) 分岐に入った時点で、その形のプロパティだけが使える "loading" の分岐で r.data と書くとエラーになる
図 1 — 判別用のプロパティを見ることで、残りのプロパティが確定する

なぜこの形にするか

「ありえない組み合わせ」を型で禁止できます。

次のように書くと、success なのに message があり data がないという状態を作れてしまいます。

// 悪い例 — すべて省略可能にしてしまう
type Result = {
	status: "success" | "error" | "loading";
	data?: string;
	message?: string;
};

この形だと、使う側は毎回 data の存在を確認しなければなりません。判別可能ユニオンなら、分岐に入った時点で確定します。

動かしてみる

// discriminated.ts
type Shape =
	| { kind: "circle"; radius: number }
	| { kind: "rect"; width: number; height: number };

function area(s: Shape): number {
	switch (s.kind) {
		case "circle": return Math.PI * s.radius ** 2;
		case "rect":   return s.width * s.height;
	}
}

console.log(area({ kind: "circle", radius: 2 }).toFixed(2));
console.log(area({ kind: "rect", width: 3, height: 4 }));
$ npx tsx discriminated.ts
12.57
12

08.4 型ガード関数

絞り込みを関数に切り出す

判定を関数にすると、戻り値の型を x is T と書くことで絞り込みを伝えられます。

type User = { name: string; age: number };

function isUser(v: unknown): v is User {
	if (typeof v !== "object" || v === null) return false;
	const o = v as Record<string, unknown>;
	return typeof o.name === "string" && typeof o.age === "number";
}

const data: unknown = JSON.parse('{"name":"Alice","age":30}');

if (isUser(data)) {
	console.log(data.name);      // User として扱える
}

v is User は「そう見なせ」という宣言で、検査されません。

関数の中身が間違っていても、TypeScript は指摘しません。return true; とだけ書いても通ります。as と同じく、正しさは書き手が保証します。その代わり、保証する場所がこの関数の中だけに閉じ込められます。

配列の絞り込み

const values: (string | undefined)[] = ["a", undefined, "b"];

// filter だけでは型が変わらない
const a = values.filter((v) => v !== undefined);     // (string | undefined)[]

// 型ガードを書くと変わる
const b = values.filter((v): v is string => v !== undefined);   // string[]

assert 関数

asserts を使うと、「ここを通過したら確定している」という形も書けます。

function assertIsUser(v: unknown): asserts v is User {
	if (!isUser(v)) {
		throw new Error("User の形ではありません");
	}
}

const data: unknown = JSON.parse(text);
assertIsUser(data);
console.log(data.name);      // ここでは User

asserts を使う関数は、明示的な型注釈が要ります。

const f = (v: unknown): asserts v is User => {...} のようにアロー関数で書くと、呼び出し側で効きません。function 宣言か、明示的に型を付けた変数として定義してください。

08.5 never による網羅性チェック

分岐の漏れをコンパイル時に見つける

never は「ありえない型」です。すべての分岐を書き切れば、最後には never だけが残ります。

type Shape =
	| { kind: "circle"; radius: number }
	| { kind: "rect"; width: number; height: number };

function area(s: Shape): number {
	switch (s.kind) {
		case "circle": return Math.PI * s.radius ** 2;
		case "rect":   return s.width * s.height;
		default: {
			const _exhaustive: never = s;     // ここに来るはずがない
			throw new Error(`未対応の形状: ${JSON.stringify(_exhaustive)}`);
		}
	}
}
すべての分岐を書いた場合 circle | rect case "circle" case "rect" default 残りは never 通る union に triangle を足した場合 — 分岐を書き忘れている circle | rect | triangle case "circle" case "rect" default triangle が残る TS2322 never に入らない union に種類を足すと、対応が必要な箇所をコンパイラが教えてくれる
図 2 — 分岐を書き切れば残りは never。書き忘れると never に代入できずエラーになる

種類を増やすとエラーになる

type Shape =
	| { kind: "circle"; radius: number }
	| { kind: "rect"; width: number; height: number }
	| { kind: "triangle"; base: number; height: number };   // ← 追加した

// area の default 節でエラーになる
//   const _exhaustive: never = s;
//         ~~~~~~~~~~~
// error TS2322: Type '{ kind: "triangle"; base: number; height: number; }'
//               is not assignable to type 'never'.
//
// 訳: 型 '{ kind: "triangle"; ... }' を型 'never' に割り当てることはできません。

これが TypeScript で最も価値のある機能のひとつです。

union に種類を追加したとき、対応が必要な箇所をコンパイラが列挙してくれます。実行してみるまで漏れに気づけない、という状況がなくなります。

判別可能ユニオンを使うなら、必ず網羅性チェックもセットで書いてください。この2つは組み合わせて初めて効果があります。

動かしてみる

// exhaustive.ts
type Event =
	| { type: "click"; x: number; y: number }
	| { type: "key"; code: string }
	| { type: "close" };

function describe(e: Event): string {
	switch (e.type) {
		case "click": return `クリック (${e.x}, ${e.y})`;
		case "key":   return `キー ${e.code}`;
		case "close": return "閉じる";
		default: {
			const _exhaustive: never = e;
			throw new Error(`未対応のイベント: ${JSON.stringify(_exhaustive)}`);
		}
	}
}

console.log(describe({ type: "click", x: 10, y: 20 }));
console.log(describe({ type: "key", code: "Enter" }));
console.log(describe({ type: "close" }));
$ npx tsx exhaustive.ts
クリック (10, 20)
キー Enter
閉じる

if 文でも使える

function describe(e: Event): string {
	if (e.type === "click") return `クリック (${e.x}, ${e.y})`;
	if (e.type === "key")   return `キー ${e.code}`;
	if (e.type === "close") return "閉じる";

	const _exhaustive: never = e;
	throw new Error(`未対応: ${JSON.stringify(_exhaustive)}`);
}