型ガードとは、if などの条件で値の型を確かめ、そのブロックの中だけ型を狭くする(絞り込む)しくみです。 たとえば string | number 型の値も、typeof v === "number" の中では number として扱えます。この絞り込みを英語で narrowing と呼び、typeof・instanceof・in・=== のような組み込みの書き方と、自分で判定関数を作る is(ユーザー定義型ガード)・asserts(アサーション関数) があります。

この記事では、絞り込みの方法11種類を一覧表と「どれを選ぶか」の判断表にまとめ、すべてのコードを TypeScript 7.0.2 と 5.9.3 の tsc で型チェックしたエラー出力を載せています。さらに、TS 5.5以降の「推論型述語」で filter が自動的に絞り込まれる様子を5.4と比べて確認し、is の実装ミスで型チェックは通るのに実行時エラーになる例をNode.js 22で実際に動かしました。コールバック内で絞り込みが消える問題など、つまずきやすい点の直し方まで扱います。unknown と any の違い、enum、ジェネリクス、Zodの使い方はそれぞれ別記事で扱っているため、ここでは「型の絞り込み」に絞ります。

型ガード(絞り込み)の早見表

方法 書き方 絞り込まれる型 向いている場面
typeof typeof v === "string" string・number・boolean・bigint・symbol・undefined・function・object プリミティブの判定
instanceof v instanceof Date そのクラスのインスタンス クラス・Error・Date
in "isbn" in item そのプロパティを持つ型 形の違うオブジェクトのユニオン
=== / !== v === null / v !== undefined 比べた値を除く・残す null・undefined・リテラル
Array.isArray Array.isArray(v) 配列 単数か配列か
truthiness if (v) null・undefined・""・0・false を除く 空文字や0も除いてよいとき
判別可能なユニオン switch (s.kind) kind が一致するメンバー 状態・イベント・APIの結果
ユーザー定義型ガード function isX(v): v is X X 判定を再利用したい/unknown の検証
アサーション関数 function assertX(v): asserts v is X 呼び出し以降ずっと X 満たさなければ例外で止めたい
never の網羅チェック default: assertNever(s) (漏れがあればエラー) ユニオンに後から追加されるとき
satisfies 値 satisfies 型 絞り込みではない(検査だけ) 定数が型に合うか確かめる

選び方の基本は「組み込みで書けるなら組み込み、同じ判定を何度も書くなら is、満たさないと先に進めないなら asserts」です。

本記事の内容は、ご自由にお使いください。
ご利用の際は、出典として本ページへのリンクを記載いただけますようお願いします。

(記載例)出典:株式会社RJC「TypeScript型ガード|is・in・assertsの使い分け一覧」

検証環境と型チェックのコマンド

この記事のコードは、次の環境とコマンドで確かめています。 各ファイルの先頭の export {}; は、ファイルを独立したモジュールとして扱うための1行です。

項目 バージョン
TypeScript 7.0.2(npmの latest)と 5.9.3。推論型述語の比較のみ 5.4.5
Node.js v22.22.2(.ts を型を取り除いて直接実行)
Zod 4.6.5(実行時検証の例のみ)
npm i -D typescript@7.0.2 @types/node@22
npx tsc --strict --noEmit --target es2023 --lib es2023 --types node --module nodenext --skipLibCheck src/basics.ts
node src/basics.ts      # Node 22.18以降は .ts をそのまま実行できる(package.json に "type": "module")

TS 7.0.2 と 5.9.3 のエラーコードと本文は、この記事の例ではすべて同じでした。 違いは、エラー時の終了コード(7.0.2は 1、5.9.3は 2)と、オーバーロードのエラーで5.9.3がすべての候補を表示するのに対し7.0.2は最後の候補だけを表示する点です。

組み込みの絞り込み7種類(typeof・instanceof・inなど)

まずは、追加の関数なしで使える絞り込みです。 絞り込みのあとは、その型だけが持つメソッドを使えるようになります。

// src/basics.ts
export {};
// 1. typeof:プリミティブの判定
function formatPrice(v: string | number): string {
  if (typeof v === "number") return v.toFixed(0) + "円";   // v: number
  return v.trim();                                         // v: string
}
// 2. instanceof:クラス(コンストラクタ)の判定
function toTime(d: Date | string): number {
  return d instanceof Date ? d.getTime() : Date.parse(d);
}
// 3. in:プロパティの有無で判定
type Book = { title: string; isbn: string };
type Pen = { name: string; color: string };
function label(item: Book | Pen): string {
  if ("isbn" in item) return `本: ${item.title}`;          // item: Book
  return `ペン: ${item.name}`;                              // item: Pen
}
// 4. === / !== :リテラルやnullとの比較
function greet(name: string | null): string {
  if (name === null) return "ゲストさん";
  return `${name}さん`;                                     // name: string
}
// 5. Array.isArray:配列かどうか
function toList(tags: string | string[]): string[] {
  return Array.isArray(tags) ? tags : [tags];
}
// 6. truthiness(真偽値としての評価)
function len(s?: string): number {
  if (s) return s.length;                                   // s: string("" は除外される)
  return 0;
}
// 7. 判別可能なユニオン(共通の kind プロパティで判定)
type Shape =
  | { kind: "circle"; r: number }
  | { kind: "rect"; w: number; h: number };
function area(s: Shape): number {
  switch (s.kind) {
    case "circle": return Math.PI * s.r ** 2;
    case "rect": return s.w * s.h;
  }
}
console.log(formatPrice(1200), formatPrice(" pen "), toTime("2026-10-01"), label({ name: "青", color: "blue" }));
console.log(greet(null), toList("a"), len(""), area({ kind: "rect", w: 2, h: 3 }));

型チェックと実行結果:

== Version 7.0.2
(exit 0)
== Version 5.9.3
(exit 0)

$ node src/basics.ts
1200円 pen 1790812800000 ペン: 青
ゲストさん [ 'a' ] 0 6

絞り込まずに使うと、次のエラーになります。 3つめは「typeof x === "object" でも null が残る」というJavaScriptの仕様によるものです。

// src/basics_err.ts
export {};
function formatPrice(v: string | number): string {
  return v.toFixed(0);                  // 絞り込み前
}
type Book = { title: string; isbn: string };
type Pen = { name: string; color: string };
function label(item: Book | Pen): string {
  return item.title;                    // どちらか分からない
}
function check(x: unknown) {
  if (typeof x === "object") {
    return Object.keys(x);              // typeof null === "object"
  }
}
== Version 7.0.2
src/basics_err.ts(3,12): error TS2339: Property 'toFixed' does not exist on type 'string | number'.
  Property 'toFixed' does not exist on type 'string'.
src/basics_err.ts(8,15): error TS2339: Property 'title' does not exist on type 'Book | Pen'.
  Property 'title' does not exist on type 'Pen'.
src/basics_err.ts(12,24): error TS2769: No overload matches this call.
  The last overload gave the following error.
    Argument of type 'object | null' is not assignable to parameter of type 'object'.
      Type 'null' is not assignable to type 'object'.
(exit 1)
== Version 5.9.3
(TS2339 の2件は同じ文面。TS2769 は次のように全候補を表示)
src/basics_err.ts(12,24): error TS2769: No overload matches this call.
  Overload 1 of 2, '(o: {}): string[]', gave the following error.
    Argument of type 'object | null' is not assignable to parameter of type '{}'.
      Type 'null' is not assignable to type '{}'.
  Overload 2 of 2, '(o: object): string[]', gave the following error.
    Argument of type 'object | null' is not assignable to parameter of type 'object'.
      Type 'null' is not assignable to type 'object'.
(exit 2)

typeof x === "object" のあとは x !== null も確かめると、object 型に絞り込まれます。

組み込みの絞り込みの注意点

方法 気をつけること こうするとよい
typeof null も "object"、配列も "object" v !== null、配列は Array.isArray
instanceof type・interface には使えない(実行時に存在しない) 形で判定するなら in か判別プロパティ
in 両方の型が持つプロパティだと絞り込めない どちらか一方にだけあるプロパティを選ぶ
truthiness 0 や "" も除外される 数値・文字列は !== undefined で比べる
== v == null は null と undefined の両方を除く 意図して両方除くときだけ使う

判別可能なユニオンとneverの網羅チェック

判別可能なユニオン(discriminated union)とは、すべてのメンバーが共通のリテラル型プロパティ(type や kind)を持つユニオン型です。 そのプロパティを switch で調べるだけで、各 case の中が正確に絞り込まれます。

さらに default で never 型を受け取る関数に渡すと、あとからメンバーを追加したときに、処理の書き忘れをコンパイル時に見つけられます。never は「値が存在しない型」で、すべての case で絞り込まれた残りは never になるはずだからです。

// src/exhaustive.ts
export {};
type Status =
  | { type: "pending" }
  | { type: "shipped"; trackingNo: string }
  | { type: "canceled"; reason: string }
  | { type: "returned"; at: string };          // あとから追加した

function assertNever(x: never): never {
  throw new Error("想定外の値: " + JSON.stringify(x));
}

function message(s: Status): string {
  switch (s.type) {
    case "pending":  return "準備中";
    case "shipped":  return `発送済み(${s.trackingNo})`;
    case "canceled": return `キャンセル: ${s.reason}`;
    default:         return assertNever(s);    // returned の処理を忘れている
  }
}
== Version 7.0.2
src/exhaustive.ts(17,41): error TS2345: Argument of type '{ type: "returned"; at: string; }' is not assignable to parameter of type 'never'.
(exit 1)
== Version 5.9.3
src/exhaustive.ts(17,41): error TS2345: Argument of type '{ type: "returned"; at: string; }' is not assignable to parameter of type 'never'.
(exit 2)

エラー文に「漏れているメンバー(type: "returned")」がそのまま表示されます。 関数を用意しない書き方も2つあります。

// src/exhaustive2.ts
export {};
type Status = { type: "pending" } | { type: "shipped" } | { type: "returned" };
// 方法2:default で satisfies never(関数を用意しなくてよい)
function m1(s: Status): string {
  switch (s.type) {
    case "pending": return "準備中";
    case "shipped": return "発送済み";
    default: s satisfies never; throw new Error("unreachable");
  }
}
// 方法3:戻り値の型を書き、default を書かない
function m2(s: Status): string {
  switch (s.type) {
    case "pending": return "準備中";
    case "shipped": return "発送済み";
  }
}
== Version 7.0.2
src/exhaustive2.ts(8,16): error TS1360: Type '{ type: "returned"; }' does not satisfy the expected type 'never'.
src/exhaustive2.ts(12,25): error TS2366: Function lacks ending return statement and return type does not include 'undefined'.
(exit 1)
== Version 5.9.3
(同じ2件のエラー)
(exit 2)
網羅チェックの方法 エラーに漏れたメンバーが出るか 実行時の守り 向いている場面
assertNever(s) 出る(TS2345) 例外を投げる 外部データが混ざる可能性があるとき
s satisfies never 出る(TS1360) 次の throw で守る 関数を増やしたくないとき
戻り値の型+default なし 出ない(TS2366) なし 値を返す小さな関数

ユーザー定義型ガード(is)とアサーション関数(asserts)

v is Member を戻り値の型に書いた関数は、true を返したときに引数を Member 型として扱わせます。 これがユーザー定義型ガードです。asserts v is Member を書いた関数は、例外を投げずに戻ってきたら、それ以降ずっと Member 型として扱わせます。 これがアサーション関数(TS 3.7以降)です。

// src/predicate.ts
export {};
type Member = { id: number; name: string };

// ユーザー定義型ガード:戻り値の型に「引数 is 型」と書く
function isMember(v: unknown): v is Member {
  return (
    typeof v === "object" && v !== null &&
    "id" in v && typeof v.id === "number" &&
    "name" in v && typeof v.name === "string"
  );
}

// アサーション関数:条件を満たさなければ例外。通過後は型が絞られる
function assertMember(v: unknown): asserts v is Member {
  if (!isMember(v)) throw new TypeError("Memberではありません: " + JSON.stringify(v));
}
function assertDefined<T>(v: T, msg = "値がありません"): asserts v is NonNullable<T> {
  if (v === undefined || v === null) throw new Error(msg);
}

const data: unknown = JSON.parse('{"id": 1, "name": "佐藤"}');
if (isMember(data)) console.log("is:", data.name.toUpperCase());     // data: Member

const data2: unknown = JSON.parse('{"id": 2, "name": "鈴木"}');
assertMember(data2);
console.log("asserts:", data2.id + 1);                               // 以降 data2: Member

const found = [10, 20].find((n) => n > 15);   // number | undefined
assertDefined(found);
console.log("assertDefined:", found.toFixed(1));

try {
  assertMember(JSON.parse('{"id": "3"}'));
} catch (e) {
  console.log((e as Error).name + ": " + (e as Error).message);
}
== Version 7.0.2
(exit 0)
== Version 5.9.3
(exit 0)

$ node src/predicate.ts
is: 佐藤
asserts: 3
assertDefined: 20.0
TypeError: Memberではありません: {"id":"3"}

"id" in v && typeof v.id === "number" のように、in で確かめたあとはそのプロパティを読めます(TS 4.9以降、in で確かめたプロパティが型に加わるため)。as を使わずに書けるので、判定の書き漏れも型チェックで見つかります。

isとassertsで起きやすいエラー

// src/predicate_err.ts
export {};
// アサーション関数をアロー関数+型注釈なしの変数で使うとエラー
const assertString = (v: unknown): asserts v is string => {
  if (typeof v !== "string") throw new Error("not string");
};
const x: unknown = "a";
assertString(x);
// 型ガードの戻り値に boolean と書くと絞り込まれない
function isNum(v: unknown): boolean { return typeof v === "number"; }
const y: unknown = 1;
if (isNum(y)) y.toFixed();
== Version 7.0.2
src/predicate_err.ts(7,1): error TS2775: Assertions require every name in the call target to be declared with an explicit type annotation.
src/predicate_err.ts(11,15): error TS18046: 'y' is of type 'unknown'.
(exit 1)
== Version 5.9.3
(同じ2件のエラー)
(exit 2)
エラー 原因 直し方
TS2775 Assertions require every name in the call target to be declared with an explicit type annotation. アサーション関数を型注釈のない const に入れた function 宣言にする、または const assertString: (v: unknown) => asserts v is string = … と変数に型を書く
TS18046 'y' is of type 'unknown'. 戻り値を boolean と書いた 戻り値を v is number にする
TS2677 A type predicate's type must be assignable to its parameter's type. (v: number): v is string のように引数と無関係な型 述語の型は引数の型の一部にする

isとassertsの使い分け

比べる点 is(型ガード) asserts(アサーション関数)
戻り値 boolean なし(void)
満たさないとき false を返す → else で別の処理 例外を投げる → 以降は実行されない
絞り込まれる範囲 if のブロックの中 呼び出し以降すべて
filter に渡す できる できない
向いている場面 混在するデータを振り分ける 前提条件のチェック、テスト、初期化

TS 5.5以降の推論型述語(filterが自動で絞り込まれる)

TS 5.5から、関数の本体から x is 型 が自動で推論されるようになりました(Inferred Type Predicates)。 これにより、filter にアロー関数を渡すだけで、結果の配列の型が絞り込まれます。

// src/inferred.ts
export {};
// TS 5.5以降:アロー関数の本体から「x is 型」が自動で推論される
const ids = [1, undefined, 3, null, 5];
const nums = ids.filter((x) => x !== undefined && x !== null);
//    ^? number[]
const total: number = nums.reduce((a, b) => a + b, 0);

type Task = { id: number; done: boolean } | { error: string };
const tasks: Task[] = [{ id: 1, done: true }, { error: "取得失敗" }];
const ok = tasks.filter((t) => "id" in t);
console.log(total, ok.map((t) => t.id));       // ok: { id: number; done: boolean }[]

// 推論された型述語は関数としても使える
const isString = (v: unknown) => typeof v === "string";
//    ^? (v: unknown) => v is string
const vals: unknown[] = ["a", 1, "b"];
console.log(vals.filter(isString).map((s) => s.toUpperCase()));

3つのバージョンで型チェックした結果です。 5.4.5では filter の結果が絞り込まれず、エラーになります。

== Version 7.0.2
(exit 0)
== Version 5.9.3
(exit 0)

== Version 5.4.5
src/inferred.ts(6,7): error TS2322: Type 'number | null | undefined' is not assignable to type 'number'.
  Type 'undefined' is not assignable to type 'number'.
src/inferred.ts(6,45): error TS18049: 'a' is possibly 'null' or 'undefined'.
src/inferred.ts(6,49): error TS18049: 'b' is possibly 'null' or 'undefined'.
src/inferred.ts(11,36): error TS2339: Property 'id' does not exist on type 'Task'.
  Property 'id' does not exist on type '{ error: string; }'.
src/inferred.ts(17,46): error TS18046: 's' is of type 'unknown'.

$ node src/inferred.ts
9 [ 1 ]
[ 'A', 'B' ]

推論された型は、宣言ファイル(.d.ts)を出力すると目で確かめられます。

// src/inferred_decl.ts
export const isString = (v: unknown) => typeof v === "string";
export const isDefined = <T>(v: T) => v !== undefined && v !== null;
export const isTruthy = (v: number | undefined) => !!v;
export const nums = [1, undefined, 3].filter((x) => x !== undefined);
npx tsc --strict --declaration --emitDeclarationOnly --target es2023 --lib es2023 --types node --module nodenext --skipLibCheck --outDir out src/inferred_decl.ts
// out/inferred_decl.d.ts(TS 7.0.2・5.9.3 とも同じ出力)
export declare const isString: (v: unknown) => v is string;
export declare const isDefined: <T>(v: T) => v is T & {};
export declare const isTruthy: (v: number | undefined) => boolean;
export declare const nums: number[];

推論されないパターン

推論されるのは「true なら型が◯◯、false なら◯◯ではない」と両方向に言える条件だけです。 !!x は 0 も false にするため、「false なら undefined」とは言えず、推論されません。

// src/inferred_err.ts
export {};
// 推論されないパターン1:truthiness(0 や "" も除外されるため「x is number」と言えない)
const counts = [0, 1, undefined, 2];
const a: number[] = counts.filter((x) => !!x);
// 推論されないパターン2:Boolean を渡す
const b: number[] = counts.filter(Boolean);
// 推論されないパターン3:false を返しても「型が違う」とは言えない条件
const c: string[] = ["a", 1, "bb"].filter((x) => typeof x === "string" && x.length > 1);
== Version 7.0.2
src/inferred_err.ts(4,7): error TS2322: Type '(number | undefined)[]' is not assignable to type 'number[]'.
  Type 'number | undefined' is not assignable to type 'number'.
    Type 'undefined' is not assignable to type 'number'.
src/inferred_err.ts(6,7): error TS2322: Type '(number | undefined)[]' is not assignable to type 'number[]'.
  Type 'number | undefined' is not assignable to type 'number'.
    Type 'undefined' is not assignable to type 'number'.
src/inferred_err.ts(8,7): error TS2322: Type '(string | number)[]' is not assignable to type 'string[]'.
  Type 'string | number' is not assignable to type 'string'.
    Type 'number' is not assignable to type 'string'.
(exit 1)
== Version 5.9.3
(同じ3件のエラー)
(exit 2)
書き方 推論されるか 代わりの書き方
filter((x) => x !== undefined) される -
filter((x) => x != null) される -
filter((t) => "id" in t) される -
filter((x) => !!x) されない x !== undefined で比べる
filter(Boolean) されない filter((x) => x !== undefined)
filter((x) => typeof x === "string" && x.length > 1) されない 2回に分ける:.filter((x) => typeof x === "string").filter((s) => s.length > 1)

isの実装ミスで型安全が崩れる例

is は「この関数が true を返したら、この型だと信じてください」という宣言で、中身が正しいかどうかはTypeScriptは確かめません。 判定が足りないと、型チェックは通るのに実行時にエラーになります。

// src/wrong_is.ts
export {};
type Item = { name: string; price: number };

// 実装ミス:name しか確かめていないのに「Item である」と宣言している
function isItem(v: unknown): v is Item {
  return typeof v === "object" && v !== null && "name" in v;
}

const rows: unknown[] = JSON.parse('[{"name":"pen","price":120},{"name":"note"}]');
for (const r of rows) {
  if (isItem(r)) {
    console.log(r.name, r.price.toFixed(0));   // 型チェックは通る
  }
}
== Version 7.0.2
(exit 0)
== Version 5.9.3
(exit 0)

$ node src/wrong_is.ts
pen 120
file:///…/src/wrong_is.ts:12
    console.log(r.name, r.price.toFixed(0));   // 型チェックは通る
                                ^

TypeError: Cannot read properties of undefined (reading 'toFixed')

型チェックは2つのバージョンとも成功し、2件目のデータで TypeError になりました。 判定に使うすべてのプロパティを確かめると、意図どおりに振り分けられます。

// src/right_is.ts
export {};
type Item = { name: string; price: number };
function isItem(v: unknown): v is Item {
  return (
    typeof v === "object" && v !== null &&
    "name" in v && typeof v.name === "string" &&
    "price" in v && typeof v.price === "number"
  );
}
const rows: unknown[] = JSON.parse('[{"name":"pen","price":120},{"name":"note"}]');
for (const r of rows) {
  if (isItem(r)) console.log(r.name, r.price.toFixed(0));
  else console.log("スキップ:", JSON.stringify(r));
}
$ node src/right_is.ts
pen 120
スキップ: {"name":"note"}

実行時検証ライブラリ(Zod)との関係

プロパティが多いデータや入れ子のデータは、Zodのような実行時検証ライブラリでスキーマを書き、そこから型と型ガードを作ると判定漏れが起きにくくなります。 スキーマが「判定ロジック」と「型」の両方の元になるためです。Zodの使い方そのものは別記事「Zodの使い方」で扱っているため、ここでは型ガードとのつなぎ方だけ示します。

// src/zod_guard.ts(zod 4.6.5)
import { z } from "zod";
const Item = z.object({ name: z.string(), price: z.number() });
type Item = z.infer<typeof Item>;
// スキーマから型ガードを作る(判定ロジックと型が1か所にまとまる)
const isItem = (v: unknown): v is Item => Item.safeParse(v).success;

const rows: unknown[] = JSON.parse('[{"name":"pen","price":120},{"name":"note"}]');
console.log(rows.filter(isItem).map((r) => r.price.toFixed(0)));
const r = Item.safeParse(rows[1]);
if (!r.success) console.log(r.error.issues.map((i) => `${i.path.join(".")}: ${i.message}`));
$ node src/zod_guard.ts
[ '120' ]
[ 'price: Invalid input: expected number, received undefined' ]
判定の作り方 判定漏れ 理由がわかるか 向いている場面
手書きの is 起きやすい(型と判定が別々) わからない(false のみ) プロパティが2〜3個の小さな型
Zodなどのスキーマ+is 起きにくい(スキーマが1つの元) わかる(issues に場所と理由) APIの応答・フォーム・設定ファイル

satisfiesは絞り込みではない

satisfies は「値がその型に合っているか検査する」だけで、型を変えたり絞り込んだりはしません。 型注釈(: 型)との違いは、変数の型がリテラルのまま残るかどうかです。

// src/satisfies.ts
export {};
type Color = "red" | "blue";
// satisfies:値が型に合っているか「検査」するだけ。値の型(リテラル)はそのまま残る
const theme = { main: "red", sub: "blue" } satisfies Record<string, Color>;
const m: "red" = theme.main;                       // OK: "red" のまま
// 型注釈:変数の型を Record<string, Color> に「広げる」
const theme2: Record<string, Color> = { main: "red", sub: "blue" };
const m2: "red" = theme2.main;                     // エラー
// satisfies は絞り込みではない:unknown を絞ることはできない
const input: unknown = "red";
const c = input satisfies Color;                   // エラー
== Version 7.0.2
src/satisfies.ts(8,7): error TS2322: Type 'Color' is not assignable to type '"red"'.
  Type '"blue"' is not assignable to type '"red"'.
src/satisfies.ts(11,17): error TS1360: Type 'unknown' does not satisfy the expected type 'Color'.
(exit 1)
== Version 5.9.3
(同じ2件のエラー)
(exit 2)
書き方 何をするか 実行時のチェック unknown に使えるか
型ガード(typeof・is など) 条件で型を絞る ある 使える
asserts 満たさなければ例外、以降は型を絞る ある 使える
satisfies 型に合うか検査するだけ(型は変えない) ない 使えない(TS1360)
as 型を上書きする(検査しない) ない 使えるが、誤りも通る

よくあるつまずきと直し方

絞り込みが「消える」「効かない」と感じる場面をまとめて型チェックしました。

// src/pitfalls.ts
export {};
type Order = { id: number; coupon?: string; items: string[] };

// 1) プロパティの絞り込みはコールバックの中で消える
function f1(order: Order) {
  if (order.coupon !== undefined) {
    order.items.forEach(() => {
      console.log(order.coupon.toUpperCase());   // エラー
    });
  }
}
// 直し方:const に取り出してから絞り込む
function f1fixed(order: Order) {
  const coupon = order.coupon;
  if (coupon !== undefined) {
    order.items.forEach(() => console.log(coupon.toUpperCase()));   // OK
  }
}

// 2) let 変数は「最後の代入のあと」なら保たれる(TS 5.4以降)
function f2(input?: string) {
  let name = input;
  if (name === undefined) name = "ゲスト";
  return () => name.length;                    // OK
}
function f2ng(input?: string) {
  let name = input;
  if (name === undefined) name = "ゲスト";
  const fn = () => name.length;                // エラー:後で再代入される
  name = input;
  return fn;
}

// 3) 判別前に分割代入すると、片方にしかないプロパティは取り出せない
type Res = { ok: true; value: number } | { ok: false; error: string };
function f3(res: Res) {
  const { ok, value } = res;                   // エラー:value は ok:false 側にない
}
function f3fixed(res: Res) {
  if (res.ok) return res.value;
  return res.error;
}

// 4) 条件を変数に入れても絞り込める(TS 4.4以降、const のみ)
function f4(v: string | number) {
  const isStr = typeof v === "string";
  if (isStr) return v.toUpperCase();           // OK
  return v.toFixed(0);
}

// 5) in で「宣言にないプロパティ」を確かめると、そのプロパティが型に加わる(TS 4.9以降)
function f5(v: object) {
  if ("price" in v) return v.price;           // v: object & Record<"price", unknown>
}
== Version 7.0.2
src/pitfalls.ts(8,19): error TS18048: 'order.coupon' is possibly 'undefined'.
src/pitfalls.ts(29,20): error TS18048: 'name' is possibly 'undefined'.
src/pitfalls.ts(37,15): error TS2339: Property 'value' does not exist on type 'Res'.
(exit 1)
== Version 5.9.3
(同じ3件のエラー)
(exit 2)

コールバックの中で絞り込みが消えるのは、コールバックがいつ呼ばれるか分からず、その間に order.coupon が書き換えられる可能性があるためです。 const に取り出せば書き換えられないことが確かなので、絞り込みが保たれます。

配列の要素の絞り込み(TS 5.5で改善)

list[i] のような要素アクセスも、list と i が再代入されない場合はTS 5.5以降で絞り込まれます。 ループ変数のように変わる i では絞り込まれません。

// src/idx.ts
export {};
function f6(list: (string | null)[], i: number) {
  if (list[i] !== null) return list[i].length;     // TS 5.5以降はOK
}
function f6let(list: (string | null)[]) {
  for (let i = 0; i < list.length; i++) {
    if (list[i] !== null) console.log(list[i].length);   // i が変わる変数
  }
}
== Version 7.0.2
src/idx.ts(7,39): error TS2531: Object is possibly 'null'.
(exit 1)
== Version 5.9.3
src/idx.ts(7,39): error TS2531: Object is possibly 'null'.
(exit 2)
== Version 5.4.5
src/idx.ts(3,32): error TS2531: Object is possibly 'null'.
src/idx.ts(7,39): error TS2531: Object is possibly 'null'.

ループの中では const item = list[i]; と変数に取り出すか、for (const item of list) で回すと絞り込まれます。

つまずき一覧

症状 エラー 原因 こうするとよい
if の中なのにコールバックでエラー TS18048 プロパティはコールバック実行時に変わりうる const に取り出す
クロージャ内の let でエラー TS18048 クロージャの後で再代入している 再代入をクロージャより前にまとめる/const
分割代入でプロパティが取れない TS2339 判別前のユニオンには共通でないプロパティがない 先に if (res.ok) で判別する
filter の結果が絞り込まれない TS2322 !!x や Boolean は推論されない x !== undefined で書く
ループ内の list[i] でエラー TS2531 i が変わる const item = list[i]
アサーション関数でエラー TS2775 型注釈のない const のアロー関数 function 宣言にする
型チェックは通るのに実行時エラー なし is の判定漏れ 全プロパティを確かめる/スキーマから作る

どれを選ぶか:判断表

状況 おすすめ 理由
string | number などプリミティブの混在 typeof 最も短く、実行時も確実
Date や独自クラス、Error の種類 instanceof クラスの判定は実行時に可能
形の違うオブジェクトのユニオン 判別プロパティ(type・kind)を設計して switch 網羅チェックと組み合わせられる
判別プロパティを付けられない既存の型 in 片方にだけあるプロパティで判定
null・undefined を除く !== null / !== undefined 0 や "" を残せる
同じ判定を何度も書く、filter で使う is(またはTS 5.5以降は推論に任せる) 再利用でき、配列も絞れる
満たさなければ処理を止めたい asserts 以降のコードがすっきりする
外部から来たデータ(JSON・フォーム) スキーマ(Zodなど)から型ガードを作る 判定漏れが起きにくく、理由も分かる
ユニオンの処理漏れを防ぎたい assertNever / satisfies never メンバー追加時にコンパイルエラー

よくある質問

Q. 型ガードと型アサーション(as)の違いは何ですか? A. 型ガードは実行時に値を確かめてから型を絞り、as は確かめずに型を上書きします。 as は誤りがあっても型チェックを通るため、外部データには型ガードかスキーマ検証を使うのがおすすめです。

Q. isとassertsはどちらを使えばよいですか? A. 「満たさないときに別の処理をする」なら is、「満たさないなら先に進めない」なら asserts です。 filter に渡したい場合は is を使います。

Q. TypeScript 5.5の推論型述語があれば、isはもう書かなくてよいですか? A. アロー関数の中身が単純な条件なら、書かなくても推論されます。 ただし !!x や Boolean は推論されず、複数のプロパティを確かめる関数や、ライブラリとして公開する関数は、is を明示しておくと意図が読み手に伝わりやすくなります。

Q. TypeScript 7で型ガードの書き方は変わりましたか? A. この記事の例では、TS 7.0.2と5.9.3でエラーの内容はすべて同じでした。 違いはエラー時の終了コード(1と2)と、オーバーロードのエラー表示の量だけでした。

まとめ

  • 型ガードは条件で型を絞るしくみ。typeof・instanceof・in・===・Array.isArray・truthiness・判別可能なユニオンが組み込み
  • 再利用する判定は is、満たさなければ止めるなら asserts。アサーション関数は function 宣言で書く
  • 判別可能なユニオン+assertNever / satisfies never で、メンバー追加時の書き忘れをコンパイル時に検出
  • TS 5.5以降は filter((x) => x !== undefined) が自動で絞り込まれる。!!x と Boolean は対象外
  • is の中身はTypeScriptが確かめない。判定漏れは実行時の TypeError になるため、全プロパティを確かめるかスキーマから作る
  • satisfies は検査だけで絞り込みではない。unknown には使えない
  • コールバック内で絞り込みが消えたら const に取り出す。TS 7.0.2と5.9.3で結果は同じ

本記事の内容は、ご自由にお使いください。
ご利用の際は、出典として本ページへのリンクを記載いただけますようお願いします。

(記載例)出典:株式会社RJC「TypeScript型ガード|is・in・assertsの使い分け一覧」

株式会社RJCでは、TypeScriptの型ガードと型の絞り込みを仲間と学び合っています。こうした知識を共有しながら、日々技術を磨いています。

株式会社RJC ― SI事業・SES事業・AI駆動開発。RJCは一緒に成長を楽しめる会社です。

WE ARE HIRING

RJCで一緒に開発しながら、
成長を楽しみませんか?

RJCは、Web・モバイル・AIを活用した開発プロジェクトで、テックリードやPM・PMOも活躍するシステム開発会社です。会社を知る、待遇を確かめる、話を聞いてみる。気になるところ見てみてください!

  • 127日年間休日
  • 12時間平均残業時間
  • 毎日ガチャ遊びココロも大切にする福利厚生。アマギフなどの賞品ラインナップ!

ほかにも、チケットレストラン、書籍読み放題、2年ごとの慰労報奨(休暇 or 金一封)、11期連続の黒字決算。

ABOUT RJC RJCがどんな会社か知る 考え方、研修、働き方、福利厚生、社員の前職まで。RJCのことが丸わかり! RJC丸わかりページへ JOB DESCRIPTION 仕事内容・待遇を見てみる 仕事内容、給与・待遇、選考の流れ。経験者も未経験も!応募前に知りたいこと、まとめました! 募集要項を見る ENTRY エントリーする エントリーは1〜2分・履歴書不要です。まずは話を聞いてみたい、という方でも歓迎です! エントリーフォームへ

RJCで一緒に開発しながら、 成長を楽しみませんか?