TypeScriptで開発をしていて、「string | numberのようなユニオン型を絞り込んで使う」「unknownで受け取ったAPIレスポンスを安全に扱いたい」という場面は日常的に発生します。このときに必要なのが型ガード(Type Guard)とnarrowing(絞り込み)の知識です。
本記事はTypeScript 5.x準拠で、typeof / instanceof / in / user-defined type guard (is) / asserts / discriminated union / 網羅性チェックまで、コピペで動く40本超のコードサンプルで網羅的に解説します。Zod・valibot・io-tsとの実戦連携、Branded Types、Reactでの活用、アンチパターンまで含めて、現役Webエンジニアの実務で必要な知識をすべて詰め込みました。
- typeof / instanceof / in 演算子による絞り込みの動作仕様
- user-defined type guard (
x is T) とassertsの使い分け - discriminated union と
never網羅性チェックの実装パターン - Zod / valibot / io-ts によるランタイム検証との連携
- 40本以上のコピペ可能TS 5.xコードサンプル(API・React・Branded Types)
- 型ガードとnarrowingの全体像
- typeof による narrowing
- instanceof による narrowing
- in 演算子による narrowing
- user-defined type guard (x is T)
- discriminated union と網羅性チェック
- asserts キーワードによるアサーション関数
- unknown と JSONパースの安全な扱い
- その他の narrowing パターン
- Branded Types と型ガード
- enum と literal union での型ガード
- Zod / valibot / io-ts によるランタイム検証連携
- React Props と APIレスポンスでの実戦活用
- アンチパターンとリファクタリング
- まとめ:型ガードを使いこなすための指針
型ガードとnarrowingの全体像
型ガードとは、TypeScriptコンパイラに対して「この変数はこの型である」とコード上で証明するための仕組みです。コンパイラはコードフロー解析(Control Flow Analysis)によって、ガード判定のtrue/falseが確定したスコープ内で型を自動的に絞り込みます。
なぜ型ガードが必要なのか
ユニオン型をそのまま使うと、両方の型に共通するプロパティしかアクセスできません。下の例ではエラーになります。
// ❌ ユニオン型のまま使うとエラー
function printLength(value: string | number) {
console.log(value.length); // Error: Property 'length' does not exist on type 'string | number'
}
これをtypeofで絞り込むと、各分岐内で個別の型として扱えます。
// ✅ 型ガードで絞り込んでから使う
function printLength(value: string | number) {
if (typeof value === "string") {
console.log(value.length); // OK: ここではvalueはstring
} else {
console.log(value.toFixed(2)); // OK: ここではvalueはnumber
}
}
本記事で扱う型ガード手法一覧
| 手法 | 主用途 | 対象 |
|---|---|---|
typeof |
プリミティブ型の判定 | string/number/boolean等 |
instanceof |
クラスインスタンスの判定 | Date/Error/独自クラス |
in |
プロパティの有無で判定 | オブジェクト型 |
user-defined (is) |
独自判定関数 | 任意の構造 |
asserts |
アサーション関数 | 早期エラー化 |
| discriminated union | タグ付き共用体の分岐 | 状態管理・API応答 |
| Equality narrowing | リテラル比較で絞り込み | リテラル型 |
| Truthiness narrowing | 真偽値判定で絞り込み | null/undefined除外 |
typeof による narrowing
typeof演算子はJavaScript由来ですが、TypeScriptはこの演算子をコード上で見つけるとその分岐内で変数の型を自動的に絞り込みます。プリミティブ型の判定で最も使われる基本テクニックです。
typeofで判定できる値の種類
typeofが返す文字列は7種類あります。TypeScriptはこれらすべてに対してnarrowingを認識します。
// typeofが返す値の一覧
typeof "hello"; // "string"
typeof 42; // "number"
typeof true; // "boolean"
typeof undefined; // "undefined"
typeof Symbol(); // "symbol"
typeof 10n; // "bigint"
typeof function(){}; // "function"
typeof {}; // "object"
typeof null; // "object" ← 要注意
typeof []; // "object" ← 配列もobject扱い
基本: string/number 判定
最もよく使うパターンです。引数のユニオン型を分岐ごとに絞り込みます。
function formatValue(value: string | number): string {
if (typeof value === "string") {
return value.toUpperCase(); // valueはstring
}
return value.toFixed(2); // valueはnumber
}
console.log(formatValue("hello")); // "HELLO"
console.log(formatValue(3.14159)); // "3.14"
3つ以上のユニオンを絞り込む
3種類以上のプリミティブが混在する場合も同じ要領で書けます。
type Primitive = string | number | boolean;
function describe(value: Primitive): string {
if (typeof value === "string") return `文字列: ${value}`;
if (typeof value === "number") return `数値: ${value}`;
return `真偽値: ${value}`; // ここではbooleanに絞り込まれている
}
typeof ‘function’ で関数を判定
引数が値か関数かで処理を分けたい場合、typeof === "function"が使えます。ReactのuseStateの更新関数(値 or 関数)を扱う際の典型です。
type SetterArg<T> = T | ((prev: T) => T);
function applyUpdate<T>(prev: T, arg: SetterArg<T>): T {
if (typeof arg === "function") {
// ここでargは (prev: T) => T に絞り込まれる
return (arg as (prev: T) => T)(prev);
}
return arg; // ここではargはT
}
const next = applyUpdate(10, (p) => p + 1); // 11
const next2 = applyUpdate(10, 99); // 99
typeof ‘object’ は要注意(nullと配列)
typeof null === "object"であり、配列も"object"です。typeof === "object"だけでは安全ではありません。
// ❌ nullを除外できていない
function getKeys(obj: object | null): string[] {
if (typeof obj === "object") {
return Object.keys(obj); // objはobject | null → ランタイムエラーの可能性
}
return [];
}
正しくはnullチェックを追加します。
// ✅ nullを除外
function getKeys(obj: object | null): string[] {
if (typeof obj === "object" && obj !== null) {
return Object.keys(obj); // ここではobjはobject(非null)
}
return [];
}
instanceof による narrowing
instanceofはクラスインスタンスの判定に使います。Date / Error / 独自クラスなど、コンストラクタを持つオブジェクトの絞り込みに最適です。
Errorクラスの絞り込み
try/catchで受け取ったerror: unknownを扱う場面は実務で頻出します。
async function fetchUser(id: string) {
try {
const res = await fetch(`/api/users/${id}`);
if (!res.ok) throw new Error(`HTTP ${res.status}`);
return await res.json();
} catch (error) {
if (error instanceof Error) {
console.error("error.message:", error.message); // ここでerrorはError
} else {
console.error("unknown error:", error);
}
}
}
独自エラークラスの階層分岐
カスタムエラーを定義しておくと、エラー種別ごとに処理を分けられます。
class NetworkError extends Error {
constructor(public status: number, msg: string) {
super(msg);
this.name = "NetworkError";
}
}
class ValidationError extends Error {
constructor(public field: string, msg: string) {
super(msg);
this.name = "ValidationError";
}
}
function handle(err: unknown): void {
if (err instanceof NetworkError) {
console.log("ネットワークエラー:", err.status);
} else if (err instanceof ValidationError) {
console.log("入力エラー:", err.field);
} else if (err instanceof Error) {
console.log("一般エラー:", err.message);
} else {
console.log("未知のエラー:", err);
}
}
Date型の絞り込み
受け取った値が文字列かDateか分からない場合、instanceof Dateで判別できます。
function toISOString(value: string | Date): string {
if (value instanceof Date) {
return value.toISOString(); // valueはDate
}
return new Date(value).toISOString(); // valueはstring
}
console.log(toISOString(new Date()));
console.log(toISOString("2026-05-26"));
クラス階層での narrowing
継承関係を持つクラス間でもinstanceofは正しく動作します。サブクラスを判定すると親クラスのチェックは省けます。
class Animal {
constructor(public name: string) {}
}
class Dog extends Animal {
bark() { return "Woof"; }
}
class Cat extends Animal {
meow() { return "Nyan"; }
}
function speak(animal: Animal) {
if (animal instanceof Dog) {
return animal.bark(); // animalはDog
}
if (animal instanceof Cat) {
return animal.meow(); // animalはCat
}
return `unknown: ${animal.name}`;
}
in 演算子による narrowing
in演算子は「指定したキーがオブジェクトに存在するか」で型を絞り込みます。クラスを持たないプレーンオブジェクトのユニオン分岐に最適です。
基本: プロパティの有無で分岐
type Cat = { meow: () => void };
type Dog = { bark: () => void };
function makeSound(pet: Cat | Dog) {
if ("meow" in pet) {
pet.meow(); // petはCat
} else {
pet.bark(); // petはDog
}
}
3種類以上のユニオンを in で分岐
3種類以上のオブジェクト型でも、各型に固有のキーがあればinで安全に分岐できます。
type Square = { side: number };
type Circle = { radius: number };
type Rect = { width: number; height: number };
function area(shape: Square | Circle | Rect): number {
if ("side" in shape) return shape.side ** 2;
if ("radius" in shape) return Math.PI * shape.radius ** 2;
return shape.width * shape.height; // shapeはRect
}
in でoptional プロパティを扱うときの落とし穴
optionalプロパティに対してinを使うと、TypeScript 4.9以降は絞り込みが緩くなる場合があります。確実に判定するには値の存在もチェックします。
type Config = {
name: string;
debug?: boolean;
};
function isDebug(c: Config): boolean {
// ❌ "debug" in c は debug:undefined でもtrueになり得る
// ✅ 値が真かどうかまで確認
return "debug" in c && c.debug === true;
}
user-defined type guard (x is T)
標準のtypeof / instanceof / inでは表現できない複雑な構造の判定には、戻り値がx is T形式の自作判定関数を作ります。これがuser-defined type guardです。
基本: シンプルなis関数
type Fish = { swim: () => void };
type Bird = { fly: () => void };
function isFish(pet: Fish | Bird): pet is Fish {
return (pet as Fish).swim !== undefined;
}
function move(pet: Fish | Bird) {
if (isFish(pet)) {
pet.swim(); // petはFishに絞り込まれる
} else {
pet.fly(); // petはBird
}
}
unknownからの型ガード
APIレスポンスのようなunknownから始まる値の検証では、user-defined type guardが必須になります。
type User = {
id: string;
name: string;
age: number;
};
function isUser(value: unknown): value is User {
return (
typeof value === "object" &&
value !== null &&
"id" in value && typeof (value as User).id === "string" &&
"name" in value && typeof (value as User).name === "string" &&
"age" in value && typeof (value as User).age === "number"
);
}
const raw: unknown = JSON.parse('{"id":"u1","name":"Tom","age":30}');
if (isUser(raw)) {
console.log(raw.name); // 型安全にアクセス可能
}
配列の型ガード
配列に対する型ガードではArray.isArrayと要素チェックを組み合わせます。
function isStringArray(value: unknown): value is string[] {
return Array.isArray(value) && value.every((v) => typeof v === "string");
}
const data: unknown = ["a", "b", "c"];
if (isStringArray(data)) {
console.log(data.map((s) => s.toUpperCase())); // 型安全
}
ジェネリックな型ガード
任意の要素型の配列を判定する汎用関数です。要素判定関数を引数で受け取ります。
function isArrayOf<T>(
value: unknown,
guard: (item: unknown) => item is T
): value is T[] {
return Array.isArray(value) && value.every(guard);
}
const isNumber = (v: unknown): v is number => typeof v === "number";
const nums: unknown = [1, 2, 3];
if (isArrayOf(nums, isNumber)) {
console.log(nums.reduce((a, b) => a + b)); // number[]として扱える
}
discriminated union と網羅性チェック
discriminated union(判別可能なユニオン)は、共通の「タグ」プロパティでユニオン型の各分岐を区別する手法です。状態管理・Redux・APIレスポンス処理など、現代TypeScript設計の中心的なパターンです。
基本: tagプロパティで分岐
type Loading = { status: "loading" };
type Success = { status: "success"; data: string };
type Failure = { status: "failure"; error: string };
type State = Loading | Success | Failure;
function render(state: State): string {
switch (state.status) {
case "loading":
return "読み込み中...";
case "success":
return `データ: ${state.data}`; // stateはSuccessに絞り込み
case "failure":
return `エラー: ${state.error}`; // stateはFailureに絞り込み
}
}
網羅性チェック(never)
switch文で全ケースを処理し忘れたまま新しい分岐型を追加すると、バグの温床になります。never型を使えば、未処理の分岐があればコンパイルエラーで気づけます。
function assertNever(value: never): never {
throw new Error(`Unreachable case: ${JSON.stringify(value)}`);
}
function render2(state: State): string {
switch (state.status) {
case "loading": return "読み込み中...";
case "success": return state.data;
case "failure": return state.error;
default:
return assertNever(state); // 全ケース網羅していればstateはnever
}
}
新しい状態を追加するとコンパイルエラー
例えばtype StateにIdleを追加した場合、render2のdefaultでassertNever(state)がエラーになり、未処理の状態に気づけます。
// 状態を追加
type Idle = { status: "idle" };
type State2 = Loading | Success | Failure | Idle;
function render3(state: State2): string {
switch (state.status) {
case "loading": return "loading";
case "success": return state.data;
case "failure": return state.error;
// ❌ "idle" を処理していないので↓でエラー
// default: return assertNever(state); // Type 'Idle' is not assignable to 'never'
default: return "todo";
}
}
Reduxスタイルのアクション分岐
reducer関数こそdiscriminated unionの真骨頂です。
type Action =
| { type: "ADD_TODO"; text: string }
| { type: "REMOVE_TODO"; id: number }
| { type: "TOGGLE_TODO"; id: number };
type Todo = { id: number; text: string; done: boolean };
function reducer(state: Todo[], action: Action): Todo[] {
switch (action.type) {
case "ADD_TODO":
return [...state, { id: Date.now(), text: action.text, done: false }];
case "REMOVE_TODO":
return state.filter((t) => t.id !== action.id);
case "TOGGLE_TODO":
return state.map((t) =>
t.id === action.id ? { ...t, done: !t.done } : t
);
}
}
tagged union vs discriminated union
「tagged union」と「discriminated union」は実質的に同じ概念で、関数型言語界隈でtagged、TypeScript界隈でdiscriminatedと呼ばれます。本記事では一貫してdiscriminated unionと呼びます。
asserts キーワードによるアサーション関数
assertsは「これが偽なら例外を投げる」型のアサーション関数を作るためのキーワードです。x is Tがtrue/falseで分岐するのに対し、assertsは関数を抜けた時点で型が確定します。
基本: asserts value is T
function assertIsString(value: unknown): asserts value is string {
if (typeof value !== "string") {
throw new Error("Not a string");
}
}
function shout(value: unknown): string {
assertIsString(value);
return value.toUpperCase(); // ここではvalueはstring
}
非null アサーション関数
nullable な値を非nullに保証する関数も自前で書けます。
function assertDefined<T>(value: T | null | undefined): asserts value is T {
if (value === null || value === undefined) {
throw new Error("value is null/undefined");
}
}
function useValue(maybe: string | null) {
assertDefined(maybe);
console.log(maybe.length); // maybeはstring
}
条件式そのものをassertする
真偽値そのものをアサートする形式も書けます。Node.jsのassertと同じ動作です。
function assert(condition: unknown, msg?: string): asserts condition {
if (!condition) throw new Error(msg ?? "Assertion failed");
}
function safeDivide(a: number, b: number): number {
assert(b !== 0, "0除算は禁止");
return a / b;
}
unknown と JSONパースの安全な扱い
anyは型チェックを完全に無効化しますが、unknownは「型ガードを通さないと何もできない」厳格な型です。APIレスポンスやJSON.parseの戻り値は必ずunknownで受けるのがベストプラクティスです。
any と unknown の違い
const a: any = JSON.parse('{"x": 1}');
console.log(a.x.y.z); // ✅ コンパイル通る(でも実行時にクラッシュの可能性)
const u: unknown = JSON.parse('{"x": 1}');
// console.log(u.x); // ❌ コンパイルエラー: Object is of type 'unknown'
// 型ガードで絞り込むまで何もできない
if (typeof u === "object" && u !== null && "x" in u) {
console.log((u as { x: unknown }).x);
}
JSONパース結果を安全に扱う
function safeParse<T>(
raw: string,
guard: (value: unknown) => value is T
): T | null {
try {
const parsed: unknown = JSON.parse(raw);
return guard(parsed) ? parsed : null;
} catch {
return null;
}
}
type Point = { x: number; y: number };
const isPoint = (v: unknown): v is Point =>
typeof v === "object" && v !== null &&
typeof (v as Point).x === "number" &&
typeof (v as Point).y === "number";
const p = safeParse('{"x":1,"y":2}', isPoint);
if (p) console.log(p.x + p.y);
fetch+型ガードの定石パターン
async function fetchTyped<T>(
url: string,
guard: (v: unknown) => v is T
): Promise<T> {
const res = await fetch(url);
if (!res.ok) throw new Error(`HTTP ${res.status}`);
const json: unknown = await res.json();
if (!guard(json)) throw new Error("Invalid response shape");
return json;
}
const user = await fetchTyped("/api/me", isUser);
console.log(user.name); // 完全に型安全
その他の narrowing パターン
Equality narrowing(等価比較で絞り込み)
===での比較もnarrowingに使われます。リテラル型の絞り込みで効果を発揮します。
type Direction = "north" | "south" | "east" | "west";
function move(dir: Direction): [number, number] {
if (dir === "north") return [0, 1];
if (dir === "south") return [0, -1];
if (dir === "east") return [1, 0];
return [-1, 0]; // dirは"west"に絞り込まれている
}
Truthiness narrowing(真偽値で絞り込み)
if (value)のような真偽判定も型を絞り込みます。null/undefinedを除外する典型用途です。
function greet(name: string | null | undefined) {
if (name) {
console.log(name.toUpperCase()); // nameはstring(non-empty)
} else {
console.log("名無しさん"); // nameはnull | undefined | ""
}
}
0や空文字の落とし穴
Truthiness narrowing は0 / "" / falseもfalsyと判定するため、numberでは要注意です。
// ❌ 0が「未指定」と誤判定される
function withDefault(n: number | undefined): number {
if (!n) return 10; // n === 0 のときも10にしてしまう
return n;
}
// ✅ undefinedだけを除外
function withDefault2(n: number | undefined): number {
if (n === undefined) return 10;
return n;
}
Nullish coalescing と narrowing
??はnullとundefinedのみをデフォルト値で置き換えます。||と違って0や""を誤って置換しません。
function port(n: number | undefined): number {
return n ?? 3000; // n === 0 のときは0を返す
}
console.log(port(0)); // 0
console.log(port(undefined)); // 3000
Array.isArray による narrowing
配列とオブジェクトの分岐にはArray.isArrayが最適です。
function flatten(value: string | string[]): string {
if (Array.isArray(value)) {
return value.join(", "); // valueはstring[]
}
return value; // valueはstring
}
console.log(flatten(["a", "b", "c"])); // "a, b, c"
console.log(flatten("hello")); // "hello"
制御フロー解析(Control Flow Analysis)の挙動
TypeScriptは早期return / throw / 例外などを認識し、それ以降のスコープで型を再計算します。
function process(input: string | null) {
if (input === null) {
throw new Error("input required");
}
// ここではinputはstringに絞り込まれている
return input.toUpperCase();
}
typeof + keyof の組み合わせ
オブジェクトのキー判定と値型判定を組み合わせると、動的なフィールドアクセスを型安全にできます。
type Settings = {
theme: "light" | "dark";
fontSize: number;
showLineNumber: boolean;
};
function getSetting<K extends keyof Settings>(
settings: Settings,
key: K
): Settings[K] {
return settings[key];
}
const s: Settings = { theme: "dark", fontSize: 14, showLineNumber: true };
const t = getSetting(s, "theme"); // "light" | "dark"
const f = getSetting(s, "fontSize"); // number
Branded Types と型ガード
同じプリミティブ型でも「意味」が違うものを区別したいときに使うのがBranded Types(ブランド型)です。UserIdとOrderIdはどちらもstringですが、混同したくないものです。
Branded Type の作り方
type Brand<T, B> = T & { __brand: B };
type UserId = Brand<string, "UserId">;
type OrderId = Brand<string, "OrderId">;
function asUserId(s: string): UserId {
// フォーマット検証等を入れる
if (!/^u_[a-z0-9]+$/.test(s)) throw new Error("invalid user id");
return s as UserId;
}
function asOrderId(s: string): OrderId {
if (!/^o_[a-z0-9]+$/.test(s)) throw new Error("invalid order id");
return s as OrderId;
}
function getUser(id: UserId) { /* ... */ }
const uid = asUserId("u_001");
getUser(uid); // ✅ OK
// getUser("u_001"); // ❌ string は UserId に代入できない
// getUser(asOrderId("o_001")); // ❌ OrderIdはUserIdではない
Branded Type + type guard
ブランド型に対するtype guardも作れます。バリデーション関数を併せて提供すると安全です。
function isUserId(value: string): value is UserId {
return /^u_[a-z0-9]+$/.test(value);
}
function processInput(raw: string) {
if (isUserId(raw)) {
// rawはUserIdに絞り込まれる
getUser(raw);
}
}
Email型・URL型などの応用
type Email = Brand<string, "Email">;
type Url = Brand<string, "Url">;
const isEmail = (s: string): s is Email => /^[^@]+@[^@]+.[^@]+$/.test(s);
const isUrl = (s: string): s is Url => {
try { new URL(s); return true; } catch { return false; }
};
function sendMail(to: Email, body: string) { /* ... */ }
const input = "tom@example.com";
if (isEmail(input)) sendMail(input, "hello"); // 型安全に呼び出し
enum と literal union での型ガード
literal union(推奨)
現代TypeScriptではenumよりもliteral unionを推奨します。tree-shake可能で、ランタイムオーバーヘッドがありません。
const ROLE = ["admin", "user", "guest"] as const;
type Role = typeof ROLE[number]; // "admin" | "user" | "guest"
function isRole(value: string): value is Role {
return (ROLE as readonly string[]).includes(value);
}
const raw = "admin";
if (isRole(raw)) {
// rawはRoleに絞り込まれる
}
enum を使う場合の型ガード
既存コードにenumがある場合は、値の集合に含まれるかどうかで判定します。
enum Status {
Draft = "draft",
Published = "published",
Archived = "archived",
}
function isStatus(value: string): value is Status {
return Object.values(Status).includes(value as Status);
}
const input2: string = "draft";
if (isStatus(input2)) {
console.log(input2); // Statusに絞り込まれる
}
Zod / valibot / io-ts によるランタイム検証連携
本格的なAPIレスポンス検証や複雑なスキーマでは、専用のバリデーションライブラリを使うほうが保守性が高くなります。3大ライブラリの使い方を見ていきます。
Zod(最も人気)
Zodはスキーマ定義から型を自動推論するライブラリで、最も人気があります。
// npm i zod
import { z } from "zod";
const UserSchema = z.object({
id: z.string(),
name: z.string().min(1),
age: z.number().int().nonnegative(),
email: z.string().email().optional(),
});
type ZUser = z.infer<typeof UserSchema>;
// = { id: string; name: string; age: number; email?: string }
const raw3: unknown = JSON.parse('{"id":"u1","name":"Tom","age":30}');
// parse: 失敗時は例外
const user2 = UserSchema.parse(raw3);
// safeParse: 失敗時はresult.success=false
const result = UserSchema.safeParse(raw3);
if (result.success) {
console.log(result.data.name);
} else {
console.error(result.error.issues);
}
Zod + 自作 type guard
Zodスキーマを使ってtype guardを作ることもできます。
function isZUser(value: unknown): value is ZUser {
return UserSchema.safeParse(value).success;
}
if (isZUser(raw3)) {
// raw3はZUserに絞り込まれる
}
valibot(軽量・モジュラー)
valibotはZodより小さく、tree-shakeに優れたモジュラー設計が特徴です。
// npm i valibot
import * as v from "valibot";
const ProductSchema = v.object({
id: v.string(),
name: v.pipe(v.string(), v.minLength(1)),
price: v.pipe(v.number(), v.minValue(0)),
});
type Product = v.InferOutput<typeof ProductSchema>;
const rawProduct: unknown = JSON.parse('{"id":"p1","name":"book","price":1000}');
const product = v.parse(ProductSchema, rawProduct);
console.log(product.name);
// safeParse 相当
const safe = v.safeParse(ProductSchema, rawProduct);
if (safe.success) console.log(safe.output.price);
io-ts(関数型風)
io-tsは関数型プログラミング志向で、fp-tsと組み合わせて使うことが多いライブラリです。
// npm i io-ts fp-ts
import * as t from "io-ts";
import { isRight } from "fp-ts/Either";
const OrderCodec = t.type({
id: t.string,
amount: t.number,
paid: t.boolean,
});
type Order = t.TypeOf<typeof OrderCodec>;
const rawOrder: unknown = JSON.parse('{"id":"o1","amount":2000,"paid":true}');
const decoded = OrderCodec.decode(rawOrder);
if (isRight(decoded)) {
console.log(decoded.right.id); // Order型として扱える
}
3ライブラリの選び方
| ライブラリ | 特徴 | バンドルサイズ | 向いている場面 |
|---|---|---|---|
| Zod | 人気・情報量豊富 | 中(~57KB) | SaaS・チーム開発・迷ったら |
| valibot | 軽量・モジュラー | 小(~13KB) | バンドルサイズ重視・エッジ |
| io-ts | 関数型志向 | 中 | fp-ts併用プロジェクト |
React Props と APIレスポンスでの実戦活用
React Props の discriminated union
「アイコンありボタン」と「テキストのみボタン」のような排他的Propsを表現するときに最適です。
type ButtonProps =
| { variant: "icon"; icon: string; label?: string }
| { variant: "text"; label: string };
function Button(props: ButtonProps) {
if (props.variant === "icon") {
return <button>{props.icon}{props.label ?? ""}</button>;
}
return <button>{props.label}</button>;
}
// 使用例
<Button variant="icon" icon="🔍" />
<Button variant="text" label="送信" />
// ❌ <Button variant="icon" /> → icon必須エラー
React useReducer + discriminated union
type CounterAction =
| { type: "increment" }
| { type: "decrement" }
| { type: "set"; value: number };
function counterReducer(state: number, action: CounterAction): number {
switch (action.type) {
case "increment": return state + 1;
case "decrement": return state - 1;
case "set": return action.value;
}
}
APIレスポンスの早期return パターン
responseオブジェクトに対する典型的なdiscriminated unionの使い方です。
type ApiResult<T> =
| { ok: true; data: T }
| { ok: false; error: string };
async function safeFetch<T>(
url: string,
guard: (v: unknown) => v is T
): Promise<ApiResult<T>> {
try {
const res = await fetch(url);
if (!res.ok) return { ok: false, error: `HTTP ${res.status}` };
const json: unknown = await res.json();
if (!guard(json)) return { ok: false, error: "shape mismatch" };
return { ok: true, data: json };
} catch (e) {
return { ok: false, error: e instanceof Error ? e.message : "unknown" };
}
}
const result2 = await safeFetch("/api/me", isUser);
if (result2.ok) {
console.log(result2.data.name); // 型安全
} else {
console.error(result2.error);
}
アンチパターンとリファクタリング
アンチパターン1: as any による絞り込み
as anyは型チェックを完全に潰すため、絶対に使わないでください。
// ❌ 悪い例
function badParse(value: unknown): User {
return value as any; // ランタイムでクラッシュの可能性
}
// ✅ 良い例: type guardで検証
function goodParse(value: unknown): User {
if (!isUser(value)) throw new Error("invalid user");
return value;
}
アンチパターン2: 二重キャスト(as unknown as T)
「コンパイラを黙らせるだけ」の二重キャストは、ほぼ全てtype guardやスキーマ検証で置き換えるべきです。
// ❌ 悪い例
const raw4 = JSON.parse('{"x":1}');
const user3 = raw4 as unknown as User; // 型はUserになるが実態は別物
// ✅ 良い例
const raw5: unknown = JSON.parse('{"x":1}');
if (isUser(raw5)) {
const user4 = raw5; // 型安全
}
アンチパターン3: 過剰な non-null アサーション(!)
// ❌ 悪い例
function getFirst(arr: number[] | undefined): number {
return arr![0]; // arrがundefinedならランタイムエラー
}
// ✅ 良い例
function getFirstSafe(arr: number[] | undefined): number | undefined {
if (!arr || arr.length === 0) return undefined;
return arr[0];
}
アンチパターン4: 型ガード関数が偽陽性を返す
user-defined type guardの戻り値x is TはTypeScriptに対する「約束」です。中身がデタラメだとランタイムで不整合が起きます。
// ❌ 危険: 検証していないのに「is T」を返す
function badGuard(v: unknown): v is User {
return true; // 何でもUserと言ってしまう
}
// ✅ 各フィールドを真面目に確認
function goodGuard(v: unknown): v is User {
return (
typeof v === "object" && v !== null &&
typeof (v as User).id === "string" &&
typeof (v as User).name === "string" &&
typeof (v as User).age === "number"
);
}
Before/After: ネストしたif地獄をdiscriminated unionに
「これloading?それともerror?それともdata?」と複数のbooleanで状態管理しているコードは、必ずdiscriminated unionで置き換えられます。
// ❌ Before: 不正な状態を表現できてしまう
type BadState = {
isLoading: boolean;
isError: boolean;
data: User | null;
error: string | null;
};
// → isLoading:true かつ data:あり かつ error:あり、のような不正状態が作れる
// ✅ After: discriminated unionで不正状態を構造的に禁止
type GoodState =
| { status: "idle" }
| { status: "loading" }
| { status: "success"; data: User }
| { status: "error"; error: string };
Before/After: 文字列比較の連鎖をswitchに
// ❌ Before
function route(path: string) {
if (path === "/") return renderHome();
if (path === "/about") return renderAbout();
if (path === "/contact") return renderContact();
return renderNotFound();
}
// ✅ After: literal union + switch + 網羅性チェック
type Path = "/" | "/about" | "/contact";
const isPath = (s: string): s is Path =>
s === "/" || s === "/about" || s === "/contact";
function route2(path: string) {
if (!isPath(path)) return renderNotFound();
switch (path) {
case "/": return renderHome();
case "/about": return renderAbout();
case "/contact": return renderContact();
default: return assertNever(path);
}
}
declare function renderHome(): void;
declare function renderAbout(): void;
declare function renderContact(): void;
declare function renderNotFound(): void;
まとめ:型ガードを使いこなすための指針
TypeScriptの型ガードは「静的型システムとランタイム値の橋渡し」を担う中核機能です。本記事で扱った40以上のコードサンプルを身につければ、以下のような効果が期待できます。
anyを使わずにunknownから始まる値を安全にハンドリングできる- discriminated unionで不正な状態を「構造的に表現不可能」にできる
- 網羅性チェックにより、新ケース追加時の漏れがコンパイル時に発覚する
- Zod等のスキーマ検証ライブラリを併用したAPIレスポンス処理ができる
- Branded Typesで意味の異なる同型値を取り違えない設計ができる
使い分けのチートシート
| 場面 | 推奨手法 |
|---|---|
| プリミティブのユニオン判定 | typeof |
| クラスインスタンス・Error判定 | instanceof |
| オブジェクトのプロパティ有無 | in |
| 複雑な構造の検証 | user-defined type guard (is) |
| 前提条件の保証 | asserts |
| 状態管理・アクション分岐 | discriminated union + switch + never |
| APIレスポンス検証 | Zod / valibot / io-ts |
| 意味の違う同型値の区別 | Branded Types |
関連記事
- TypeScript型の基礎完全ガイド〜プリミティブ・オブジェクト・配列・ユニオン・リテラル【2026年版】〜
- TypeScriptジェネリクス完全ガイド〜関数・クラス・React・実用パターン20選〜【2026年版】
- TypeScript Utility Types完全リファレンス〜Partial・Pick・Omit・Record・実用25パターン【2026年版】〜
型ガードは覚えれば覚えるほど、コードが安全になり、リファクタリングが怖くなくなるテクニックです。今日からはまず「APIレスポンスをunknownで受けて、user-defined type guardかZodで検証してから使う」習慣を身につけてみてください。それだけで現場のバグが目に見えて減ります。

コメント