- TypeScript
TypeScript型ガード|is・in・assertsの使い分け一覧
型ガードとは、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期連続の黒字決算。