TypeScriptの型縮小を早見表で整理する in instanceof is assertsの使い分け
TypeScript で実務開発を進めていると、「この変数、もう少し型を絞り込めたら安全なのに...」と感じる場面に何度も遭遇します。特にユニオン型を扱うとき、型安全性を保ちながら適切な処理を書くのは意外と難しいものです。
私自身、過去に「型ガードを使えばいいのはわかるけど、in、instanceof、is、asserts の使い分けが曖昧で、コードレビューで何度も指摘された」という経験があります。そこで本記事では、型縮小(Type Narrowing)の主要パターンを早見表で整理し、実務での判断基準と、私が実際にハマった落とし穴をご紹介します。
この記事は、実案件で型安全なコードを書く必要があるエンジニアの方、特に「チートシートとして使える実践的な情報がほしい」という方の判断に役立つ内容です。
検証環境
本記事では、以下の環境で動作確認を行っています。
- OS: macOS Sequoia 15.2
- Node.js: 22.12.0
- TypeScript: 5.7.2
- 主要パッケージ:
- ts-node: 10.9.2
- @types/node: 22.10.2
- 検証日: 2025 年 12 月 26 日
型縮小パターン完全早見表
TypeScript の型縮小で実務によく使う 4 つの主要パターンを、判断基準と併せて一覧化しました。
パターン別の基本情報
| # | パターン | 構文例 | 主な用途 | 実行コスト | 実装難易度 |
|---|---|---|---|---|---|
| 1 | in オペレータ | 'prop' in obj | オブジェクトのプロパティ存在判定 | 低 | 易 |
| 2 | instanceof | obj instanceof Class | クラスインスタンス判定 | 低 | 易 |
| 3 | is 型ガード | (x): x is Type | カスタム条件による柔軟な判定 | 中 | 中 |
| 4 | asserts | asserts x is Type | バリデーション + 型保証の統合 | 高 | 高 |
メリットと注意点の比較表
| パターン | ✅ メリット | ⚠️ 注意点・制約 |
|---|---|---|
in | ・記述がシンプル・TypeScript が自動推論・実行コストが低い・Union 型の判定に最適 | ・optional プロパティでは効果薄・プロパティ名は文字列リテラル必須・変数を使った動的判定は不可 |
instanceof | ・継承チェーンに対応・オブジェクトの出自が明確・実行コストが低い・Date、Array など組み込み型も判定可 | ・プリミティブ型では使用不可・クラス設計への依存が生じる・プロトタイプチェーンの理解が必要 |
is | ・柔軟性が非常に高い・ロジックの再利用可能・複雑な条件に対応・any や unknown からの型縮小に有効 | ・実装コストが高め・条件の妥当性は開発者の責任・誤った実装が型安全性を損なう |
asserts | ・型安全性とランタイムチェックを統合・バリデーション処理が明確・early return 不要で見通しが良い | ・例外処理の設計が必要・パフォーマンスへの配慮要・try-catch が必須 |
使い分けの判断フローチャート
各パターンをどのように選択すべきか、判断基準を図で示します。
mermaidflowchart TD
start["型縮小が必要"] --> simple{"シンプルな<br/>プロパティ判定?"}
simple --|Yes|--> in_choice["in オペレータ"]
simple --|No|--> class_check{"クラスの<br/>インスタンス?"}
class_check --|Yes|--> instanceof_choice["instanceof"]
class_check --|No|--> complex{"複雑な条件や<br/>再利用性重視?"}
complex --|Yes|--> is_choice["is 型ガード"]
complex --|No|--> validation{"バリデーション<br/>必須?"}
validation --|Yes|--> asserts_choice["asserts"]
validation --|No|--> other["typeof など<br/>その他手法"]
実務での推奨パターン
| 状況 | 推奨パターン | 理由 |
|---|---|---|
| API レスポンスの success/error 判定 | in | プロパティの有無で判定可能、シンプル |
| クラスベースの継承関係を判定 | instanceof | 継承チェーンを正確に辿れる |
| 外部 API の unknown データ検証 | is + asserts | 柔軟な条件判定とバリデーション |
| ユーザー入力のフォームデータ検証 | asserts | 例外で不正データを早期検出 |
| 配列要素の型絞り込み | is | filter と組み合わせて型安全な配列生成 |
TypeScript 型縮小の必要性と背景
実務で直面する型の曖昧さ
実際の開発では、API レスポンスやユーザー入力など、実行時まで正確な型が確定しないデータを扱う場面が頻繁にあります。
私が以前担当した EC サイトのプロジェクトでは、商品データが「通常商品」と「セール商品」で異なるプロパティを持っており、Union 型で定義していました。
typescript// 商品データの型定義
type RegularProduct = {
id: string;
name: string;
price: number;
};
type SaleProduct = {
id: string;
name: string;
originalPrice: number;
salePrice: number;
discountRate: number;
};
type Product = RegularProduct | SaleProduct;
このとき、以下のような処理を書こうとして、TypeScript から怒られた経験があります。
typescriptfunction displayPrice(product: Product) {
// ❌ エラー: Property 'salePrice' does not exist on type 'Product'
console.log(product.salePrice);
}
これは、TypeScript が「Product 型は RegularProduct または SaleProduct のどちらかだが、salePrice は SaleProduct にしか存在しない」と判断するためです。
型縮小が解決する課題
型縮小を使うことで、条件分岐内で型を具体的に絞り込み、安全にプロパティへアクセスできます。
typescriptfunction displayPrice(product: Product) {
if ("salePrice" in product) {
// この分岐内では product は SaleProduct 型
console.log(`セール価格: ${product.salePrice}円`);
console.log(`割引率: ${product.discountRate}%`);
} else {
// この分岐では product は RegularProduct 型
console.log(`通常価格: ${product.price}円`);
}
}
型縮小の概念を図で表現すると、以下のようになります。
mermaidflowchart TD
union["Union型<br/>RegularProduct | SaleProduct"] --> check{"条件チェック<br/>'salePrice' in product"}
check -->|true| sale["SaleProduct型に縮小<br/>salePrice にアクセス可能"]
check -->|false| regular["RegularProduct型に縮小<br/>price にアクセス可能"]
型縮小を活用することで得られるメリットは以下のとおりです。
- 型安全性の向上: 実行時エラーの予防
- 開発体験の改善: IDE の補完機能が正確に働く
- コードの可読性向上: 意図が明確になり、レビュアーにも伝わりやすい
実務で遭遇する型縮小の課題
パターンが多すぎて選択に迷う
TypeScript には型縮小の手法が複数あり、初学者だけでなく、経験者でも「どれを使うべきか」の判断に迷います。
私自身、以下のような状況でパターン選択を誤り、コードレビューで指摘された経験があります。
- 失敗例 1:
instanceofを使うべきところでinを使い、継承関係を正しく判定できなかった - 失敗例 2: シンプルな判定で
is型ガードを使い、過剰な実装になってしまった - 失敗例 3: バリデーションが必要な場面で
inだけで済ませ、実行時エラーが発生した
optional プロパティでのハマりどころ
optional プロパティ(? 付き)を持つ型で in オペレータを使うと、期待通りに動作しないケースがあります。
typescripttype UserProfile = {
name: string;
age?: number; // optional プロパティ
};
以下のコードは、一見正しそうに見えますが、実は問題があります。
typescriptfunction printAge(profile: UserProfile) {
if ("age" in profile) {
// profile.age は number | undefined のまま
console.log(profile.age.toFixed()); // ❌ 実行時エラーの可能性
}
}
このコードを実行すると、age プロパティが存在しても値が undefined の場合、実行時エラーが発生します。
実際に私が本番環境で遭遇したエラーは、次のようなものでした。
arduinoTypeError: Cannot read properties of undefined (reading 'toFixed')
正しくは、以下のように undefined チェックも追加する必要があります。
typescriptfunction printAge(profile: UserProfile) {
if ("age" in profile && profile.age !== undefined) {
// profile.age は number 型
console.log(profile.age.toFixed()); // ✅ 安全
}
}
複雑な条件での型縮小の限界
実務では、複数の条件を組み合わせた複雑な型判定が必要になるケースがあります。
例えば、API レスポンスが以下のような複雑な構造の場合です。
typescripttype ApiResponse =
| { status: "success"; data: { id: number; name: string } }
| { status: "error"; error: { code: string; message: string } }
| { status: "pending"; retryAfter: number };
このような場合、in や instanceof だけでは対応しきれず、カスタム型ガード(is)やアサーション関数(asserts)が必要になります。
型縮小パターンの詳細解説と使い分け
それでは、4 つの主要パターンについて、実務での使い方と注意点を詳しく見ていきましょう。
プロパティ存在で判定する in オペレータ
in オペレータの基本構文
in オペレータは、オブジェクトに特定のプロパティが存在するかを判定し、型を縮小します。
まず、基本的な型定義を確認しましょう。
typescript// 動物の型定義
type Cat = {
name: string;
meow: () => void;
};
type Dog = {
name: string;
bark: () => void;
};
type Animal = Cat | Dog;
プロパティ存在チェックによる型縮小の実装です。
typescriptfunction makeSound(animal: Animal) {
if ("meow" in animal) {
// animal は Cat 型に縮小
animal.meow();
} else {
// animal は Dog 型に縮小
animal.bark();
}
}
✓ 動作確認済み(TypeScript 5.7.2 / Node.js 22.x)
API レスポンス処理での実践例
実務でよくあるパターンとして、API レスポンスの成功/失敗判定があります。
型定義は以下のとおりです。
typescript// API レスポンスの型定義
type SuccessResponse = {
status: "success";
data: {
id: number;
name: string;
};
};
type ErrorResponse = {
status: "error";
message: string;
code: number;
};
type ApiResponse = SuccessResponse | ErrorResponse;
レスポンス処理の実装では、data プロパティの存在で判定できます。
typescriptfunction handleApiResponse(response: ApiResponse) {
if ("data" in response) {
// SuccessResponse 型として処理
console.log(`成功: ユーザー ${response.data.name} を取得`);
return response.data;
} else {
// ErrorResponse 型として処理
console.error(`エラー ${response.code}: ${response.message}`);
throw new Error(response.message);
}
}
✓ 動作確認済み(TypeScript 5.7.2 / Node.js 22.x)
in オペレータの制限と注意点
文字列リテラル必須の制約
プロパティ名は文字列リテラルで指定する必要があり、変数を使った動的判定はできません。
typescript// ❌ 変数を使用した場合は型縮小されない
const property = "meow";
if (property in animal) {
// animal の型は Animal のまま(縮小されない)
}
正しくは、文字列リテラルを直接使用します。
typescript// ✅ 文字列リテラルを直接使用
if ("meow" in animal) {
// 正常に Cat 型に縮小される
}
optional プロパティでの挙動
optional プロパティでは、プロパティが存在しても値が undefined の可能性が残ります。
typescripttype OptionalProp = {
name: string;
age?: number; // optional プロパティ
};
以下のコードは、実行時エラーのリスクがあります。
typescriptfunction checkOptional(obj: OptionalProp) {
if ("age" in obj) {
// obj.age は number | undefined のまま
console.log(obj.age.toString()); // ❌ エラーの可能性
}
}
正しくは、undefined チェックも併用します。
typescriptfunction checkOptional(obj: OptionalProp) {
if ("age" in obj && obj.age !== undefined) {
// obj.age は number 型
console.log(obj.age.toString()); // ✅ 安全
}
}
クラスインスタンスを判定する instanceof オペレータ
instanceof の基本的な使い方
instanceof オペレータは、オブジェクトが特定のクラスのインスタンスかを判定します。
基本的なクラス定義の例です。
typescript// ユーザークラスの定義
class User {
constructor(public name: string) {}
greet() {
return `こんにちは、${this.name} です`;
}
}
管理者クラスの定義です。
typescriptclass Admin {
constructor(
public name: string,
public permissions: string[],
) {}
manageUsers() {
return `${this.name} がユーザーを管理中`;
}
}
instanceof を使った型縮小の実装は以下のとおりです。
typescripttype UserType = User | Admin;
function handleUser(user: UserType) {
if (user instanceof Admin) {
// user は Admin 型
console.log(user.manageUsers());
console.log(`権限: ${user.permissions.join(", ")}`);
} else {
// user は User 型
console.log(user.greet());
}
}
使用例です。
typescriptconst regularUser = new User("田中");
const adminUser = new Admin("佐藤", ["read", "write", "delete"]);
handleUser(regularUser); // "こんにちは、田中 です"
handleUser(adminUser); // "佐藤 がユーザーを管理中"
✓ 動作確認済み(TypeScript 5.7.2 / Node.js 22.x)
継承関係での活用
instanceof は継承チェーンを正しく辿れるため、クラス継承がある設計で威力を発揮します。
基底クラスの定義です。
typescriptclass Animal {
constructor(public name: string) {}
makeSound() {
return `${this.name} が音を出している`;
}
}
派生クラス(犬)の定義です。
typescriptclass Dog extends Animal {
breed: string;
constructor(name: string, breed: string) {
super(name);
this.breed = breed;
}
bark() {
return `${this.name}(${this.breed})がワンワン`;
}
}
派生クラス(猫)の定義です。
typescriptclass Cat extends Animal {
constructor(
name: string,
public isIndoor: boolean,
) {
super(name);
}
meow() {
const location = this.isIndoor ? "室内" : "外";
return `${this.name} が ${location} でニャーニャー`;
}
}
継承チェーンでの型縮小処理の実装です。
typescriptfunction animalAction(animal: Animal) {
if (animal instanceof Dog) {
// Dog 型に縮小(Animal のメソッドも使用可能)
console.log(animal.bark());
console.log(`犬種: ${animal.breed}`);
} else if (animal instanceof Cat) {
// Cat 型に縮小
console.log(animal.meow());
console.log(`飼育環境: ${animal.isIndoor ? "室内" : "屋外"}`);
} else {
// 基底クラス Animal として処理
console.log(animal.makeSound());
}
}
✓ 動作確認済み(TypeScript 5.7.2 / Node.js 22.x)
プリミティブ型での制限
instanceof はオブジェクト型でのみ有効で、プリミティブ型では期待通りに動作しません。
以下のコードは、正常に動作しない例です。
typescript// ❌ プリミティブ型では正常に動作しない
function checkPrimitive(value: string | number) {
if (value instanceof String) {
// 通常の文字列リテラルは String クラスのインスタンスではない
}
}
プリミティブ型には typeof を使用するのが正解です。
typescript// ✅ プリミティブ型には typeof を使用
function checkPrimitiveCorrect(value: string | number) {
if (typeof value === "string") {
// 正常に string 型に縮小される
console.log(value.toUpperCase());
} else {
// number 型として処理
console.log(value.toFixed(2));
}
}
ただし、組み込みオブジェクト(Date、Array など)は instanceof で判定可能です。
typescript// Date オブジェクトの判定(✅ 正常動作)
function processValue(value: string | Date) {
if (value instanceof Date) {
// Date 型に縮小
console.log(value.toISOString());
} else {
// string 型として処理
console.log(value.length);
}
}
配列の判定も可能です。
typescript// Array の判定も可能(✅ 正常動作)
function handleData(data: string | string[]) {
if (data instanceof Array) {
// string[] 型に縮小
console.log(`配列の長さ: ${data.length}`);
data.forEach((item) => console.log(item));
} else {
// string 型として処理
console.log(`文字列: ${data}`);
}
}
✓ 動作確認済み(TypeScript 5.7.2 / Node.js 22.x)
柔軟な条件判定を実現する is 型ガード
ユーザー定義型ガードの作成
is キーワードを使ったカスタム型ガードは、独自の条件ロジックで型縮小を実現する強力な手法です。
基本的な型定義から見てみましょう。
typescript// ユーザーと管理者の型定義
interface User {
id: number;
name: string;
email: string;
}
interface Admin {
id: number;
name: string;
permissions: string[];
}
type UserOrAdmin = User | Admin;
is キーワードを使った型ガード関数の実装です。
typescript// Admin 型かどうかを判定する型ガード関数
function isAdmin(user: UserOrAdmin): user is Admin {
// permissions プロパティの存在で判定
return "permissions" in user;
}
User 型の判定関数も同様に作成します。
typescript// User 型かどうかを判定する型ガード関数
function isUser(user: UserOrAdmin): user is User {
// email プロパティの存在で判定
return "email" in user;
}
型ガード関数を使用した処理の実装例です。
typescriptfunction processUserData(user: UserOrAdmin) {
if (isAdmin(user)) {
// user は Admin 型として扱われる
console.log(`管理者: ${user.name}`);
console.log(`権限: ${user.permissions.join(", ")}`);
} else if (isUser(user)) {
// user は User 型として扱われる
console.log(`ユーザー: ${user.name}`);
console.log(`メール: ${user.email}`);
}
}
✓ 動作確認済み(TypeScript 5.7.2 / Node.js 22.x)
複雑な条件での型縮小
より複雑な判定ロジックを含む型ガードの例を見てみましょう。
API レスポンス型の定義です。
typescript// 成功レスポンスの型定義
interface ApiSuccessResponse {
success: true;
data: {
id: number;
timestamp: string;
};
metadata?: {
version: string;
cached: boolean;
};
}
エラーレスポンスの型定義です。
typescriptinterface ApiErrorResponse {
success: false;
error: {
code: string;
message: string;
};
retryAfter?: number;
}
type ApiResponse = ApiSuccessResponse | ApiErrorResponse;
成功レスポンスかどうかを詳細に判定する型ガード関数です。
typescriptfunction isSuccessResponse(
response: ApiResponse,
): response is ApiSuccessResponse {
return (
response.success === true &&
"data" in response &&
typeof response.data === "object" &&
response.data !== null &&
"id" in response.data &&
"timestamp" in response.data
);
}
エラーレスポンスの判定関数です。
typescriptfunction isErrorResponse(response: ApiResponse): response is ApiErrorResponse {
return (
response.success === false &&
"error" in response &&
typeof response.error === "object" &&
"code" in response.error &&
"message" in response.error
);
}
型ガードを活用したエラーハンドリングの実装例です。
typescriptasync function handleApiResponse(response: ApiResponse) {
if (isSuccessResponse(response)) {
// ApiSuccessResponse 型として安全に処理
console.log(`データ ID: ${response.data.id}`);
console.log(`取得時刻: ${response.data.timestamp}`);
if (response.metadata) {
console.log(`バージョン: ${response.metadata.version}`);
console.log(`キャッシュ: ${response.metadata.cached ? "あり" : "なし"}`);
}
return response.data;
} else if (isErrorResponse(response)) {
// ApiErrorResponse 型として処理
console.error(`エラー ${response.error.code}: ${response.error.message}`);
if (response.retryAfter) {
console.log(`${response.retryAfter} 秒後にリトライ可能`);
}
throw new Error(`API Error: ${response.error.message}`);
}
}
✓ 動作確認済み(TypeScript 5.7.2 / Node.js 22.x)
再利用可能な型ガード設計
汎用的で再利用しやすい型ガード関数の設計パターンをご紹介します。
汎用的なプロパティ存在チェック型ガードです。
typescriptfunction hasProperty<T, K extends string>(
obj: T,
prop: K,
): obj is T & Record<K, unknown> {
return typeof obj === "object" && obj !== null && prop in obj;
}
型付きプロパティチェックの実装です。
typescriptfunction hasPropertyOfType<T, K extends string, V>(
obj: T,
prop: K,
type: string,
): obj is T & Record<K, V> {
return hasProperty(obj, prop) && typeof (obj as any)[prop] === type;
}
汎用型ガードの活用例です。
typescript// 様々なオブジェクト型で再利用可能
function processUnknownObject(obj: unknown) {
if (hasPropertyOfType<unknown, "name", string>(obj, "name", "string")) {
// obj は { name: string } を含む型として扱われる
console.log(`名前: ${obj.name}`);
}
if (hasPropertyOfType<unknown, "age", number>(obj, "age", "number")) {
// obj は { age: number } を含む型として扱われる
console.log(`年齢: ${obj.age} 歳`);
}
}
✓ 動作確認済み(TypeScript 5.7.2 / Node.js 22.x)
バリデーションと型保証を統合する asserts アサーション
asserts の基本構文
asserts キーワードを使用したアサーション関数は、条件が満たされない場合に例外を投げることで型縮小を実現します。
基本的なアサーション関数の構文です。
typescript// 基本的なアサーション関数
function assertIsString(value: unknown): asserts value is string {
if (typeof value !== "string") {
throw new Error(`Expected string, got ${typeof value}`);
}
}
使用例を見てみましょう。
typescriptfunction processStringValue(input: unknown) {
assertIsString(input);
// この行以降、input は string 型として扱われる
console.log(input.toUpperCase());
console.log(`文字数: ${input.length}`);
}
✓ 動作確認済み(TypeScript 5.7.2 / Node.js 22.x)
null・undefined チェックのアサーション
null・undefined チェック用のアサーション関数です。
typescriptfunction assertNotNull<T>(value: T | null | undefined): asserts value is T {
if (value === null || value === undefined) {
throw new Error("Value must not be null or undefined");
}
}
配列要素の存在チェック用アサーションです。
typescriptfunction assertArrayHasElements<T>(arr: T[]): asserts arr is [T, ...T[]] {
if (arr.length === 0) {
throw new Error("Array must have at least one element");
}
}
実践的な使用例として、ユーザーデータ処理を見てみましょう。
typescriptfunction processUserData(userData: unknown) {
// データの存在確認
assertNotNull(userData);
// オブジェクトかどうかの確認
assertIsObject(userData);
// 必要なプロパティの存在と型の確認
assertHasProperty(userData, "id");
assertIsString(userData.id);
assertHasProperty(userData, "name");
assertIsString(userData.name);
// この段階で userData は安全に使用可能
console.log(`ユーザー ID: ${userData.id}`);
console.log(`ユーザー名: ${userData.name}`);
}
補助的なアサーション関数の実装です。
typescriptfunction assertIsObject(
value: unknown,
): asserts value is Record<string, unknown> {
if (typeof value !== "object" || value === null) {
throw new Error("Expected object");
}
}
プロパティ存在確認のアサーション関数です。
typescriptfunction assertHasProperty<T extends Record<string, unknown>, K extends string>(
obj: T,
prop: K,
): asserts obj is T & Record<K, unknown> {
if (!(prop in obj)) {
throw new Error(`Expected property '${prop}' to exist`);
}
}
✓ 動作確認済み(TypeScript 5.7.2 / Node.js 22.x)
例外処理との組み合わせ
アサーション関数とエラーハンドリングを組み合わせた実践的なパターンを見てみましょう。
カスタムエラークラスの定義です。
typescriptclass ValidationError extends Error {
constructor(
message: string,
public field: string,
public value: unknown,
) {
super(message);
this.name = "ValidationError";
}
}
詳細なエラー情報を含むメールアドレス検証のアサーション関数です。
typescriptfunction assertIsEmail(value: unknown): asserts value is string {
if (typeof value !== "string") {
throw new ValidationError(
`Expected email to be string, got ${typeof value}`,
"email",
value,
);
}
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
if (!emailRegex.test(value)) {
throw new ValidationError(`Invalid email format: ${value}`, "email", value);
}
}
年齢検証のアサーション関数です。
typescriptfunction assertIsAge(value: unknown): asserts value is number {
if (typeof value !== "number") {
throw new ValidationError(
`Expected age to be number, got ${typeof value}`,
"age",
value,
);
}
if (value < 0 || value > 150) {
throw new ValidationError(
`Age must be between 0 and 150, got ${value}`,
"age",
value,
);
}
}
包括的なバリデーション処理の実装例です。
検証済みユーザーの型定義です。
typescriptinterface ValidatedUser {
email: string;
age: number;
name: string;
}
ユーザー作成関数の実装です。
typescriptfunction createUser(userData: unknown): ValidatedUser {
try {
assertIsObject(userData);
// email のバリデーション
assertHasProperty(userData, "email");
assertIsEmail(userData.email);
// age のバリデーション
assertHasProperty(userData, "age");
assertIsAge(userData.age);
// name のバリデーション
assertHasProperty(userData, "name");
assertIsString(userData.name);
if (userData.name.trim().length === 0) {
throw new ValidationError("Name cannot be empty", "name", userData.name);
}
// 全ての検証が通過した場合、安全に型変換
return {
email: userData.email,
age: userData.age,
name: userData.name.trim(),
};
} catch (error) {
if (error instanceof ValidationError) {
console.error(`バリデーションエラー in ${error.field}: ${error.message}`);
}
throw error;
}
}
✓ 動作確認済み(TypeScript 5.7.2 / Node.js 22.x)
実務での使い分けと私が学んだ教訓
ここまで 4 つの型縮小パターンを詳しく見てきましたが、実際の開発ではどのように使い分けるのが良いでしょうか。私の経験を踏まえて、実践的な判断基準をご紹介します。
私が過去に失敗したパターン選択
失敗例 1: instanceof を使うべき場面で in を使った
あるプロジェクトで、継承関係があるクラスを扱う際、以下のようなコードを書いてしまいました。
typescript// ❌ 失敗例
class BaseUser {
constructor(public name: string) {}
}
class PremiumUser extends BaseUser {
constructor(
name: string,
public features: string[],
) {
super(name);
}
}
function checkUser(user: BaseUser) {
if ("features" in user) {
// これでは継承関係を正しく判定できない
console.log((user as PremiumUser).features);
}
}
このコードの問題点は、in オペレータでは継承関係を正しく辿れないため、型安全性が損なわれる点です。
正しくは、instanceof を使うべきでした。
typescript// ✅ 正しい実装
function checkUser(user: BaseUser) {
if (user instanceof PremiumUser) {
// 正確に PremiumUser 型に縮小される
console.log(user.features);
}
}
失敗例 2: シンプルな判定で is 型ガードを過剰に使った
別のプロジェクトでは、シンプルな判定に is 型ガードを使い、コードレビューで「過剰実装」と指摘されました。
typescript// ❌ 過剰な実装
function isSuccessResponse(response: {
status: string;
}): response is { status: "success" } {
return response.status === "success";
}
// この程度なら in オペレータで十分
if ("data" in response) {
// ...
}
is 型ガードは、複雑な条件や再利用が必要な場合に使うべきで、シンプルな判定では in や instanceof の方が可読性が高いという教訓を得ました。
失敗例 3: バリデーションが必要な場面で in だけで済ませた
外部 API からのデータ検証で、in オペレータだけで済ませ、実行時エラーが発生したことがあります。
typescript// ❌ バリデーション不足
function processExternalData(data: unknown) {
if (typeof data === "object" && data !== null && "userId" in data) {
// data.userId が string かどうかの保証がない
const userId = (data as any).userId;
console.log(userId.toUpperCase()); // 実行時エラーの可能性
}
}
この場合、asserts アサーション関数を使い、型と値の両方を厳密に検証すべきでした。
typescript// ✅ 正しい実装
function assertHasUserId(data: unknown): asserts data is { userId: string } {
if (
typeof data !== "object" ||
data === null ||
!("userId" in data) ||
typeof (data as any).userId !== "string"
) {
throw new Error("Invalid data: userId must be a string");
}
}
function processExternalData(data: unknown) {
assertHasUserId(data);
// data.userId は string 型として安全に使用可能
console.log(data.userId.toUpperCase());
}
パターン別の推奨ユースケース
私の経験から、各パターンの推奨ユースケースを整理しました。
| パターン | 推奨する場面 | 推奨しない場面 |
|---|---|---|
in | ・Union 型のオブジェクトで特定プロパティの有無で判定・API レスポンスの success/error 判定・シンプルな条件分岐 | ・クラスの継承関係がある場合・外部データの厳密なバリデーション・複雑な条件判定 |
instanceof | ・クラスベースの型判定・継承関係の処理・Date、Array などの組み込み型判定 | ・プリミティブ型の判定・インターフェースの判定・クラス設計がない場合 |
is | ・複雑な条件ロジック・再利用可能な型チェック関数・unknown や any からの型縮小 | ・シンプルな判定・一度しか使わない判定・実装コストをかけたくない場合 |
asserts | ・外部データのバリデーション・ユーザー入力の検証・例外処理と型保証の統合 | ・パフォーマンスが重要な場面・例外を投げたくない場合・シンプルな型判定 |
複数パターンの組み合わせ実践例
実務では、複数の型縮小手法を組み合わせて使用することが多いです。
以下は、私が実際に書いた API サービスクラスの例です。
typescript// 複数パターンの組み合わせ例
class ApiService {
// アサーション関数でデータの妥当性を保証
private assertValidResponse(
data: unknown
): asserts data is { status: string; result: unknown } {
if (typeof data !== 'object' || data === null) {
throw new Error('Invalid response format')
}
if (!('status' in data) || !('result' in data)) {
throw new Error('Missing required response fields')
}
}
型ガードで詳細な型判定を行います。
typescript // 型ガードで詳細な型判定
private isErrorResponse(response: {
status: string
result: unknown
}): response is {
status: 'error'
result: { message: string; code: number }
} {
return (
response.status === 'error' &&
typeof response.result === 'object' &&
response.result !== null &&
'message' in response.result &&
'code' in response.result
)
}
成功レスポンスの型ガードです。
typescript private isSuccessResponse<T>(response: {
status: string
result: unknown
}): response is { status: 'success'; result: T } {
return response.status === 'success'
}
API 呼び出し処理の実装です。
typescript async processApiCall<T>(url: string): Promise<T> {
const response = await fetch(url)
const data = await response.json()
// まずアサーション関数で基本構造を確認
this.assertValidResponse(data)
// 次に型ガードで詳細な型判定
if (this.isErrorResponse(data)) {
throw new Error(`API Error ${data.result.code}: ${data.result.message}`)
} else if (this.isSuccessResponse<T>(data)) {
return data.result
} else {
throw new Error('Unexpected response format')
}
}
}
✓ 動作確認済み(TypeScript 5.7.2 / Node.js 22.x)
このコードでは、以下の順序でパターンを組み合わせています。
assertsでレスポンスの基本構造を検証is型ガードで詳細な型判定と分岐処理
この順序により、型安全性とコードの可読性を両立できました。
よくあるエラーと対処法
型縮小を実装する際に、私が実際に遭遇したエラーと解決方法をご紹介します。
エラー 1: Property does not exist on type
発生状況
Union 型で型縮小が不完全な場合に発生します。
goerror TS2339: Property 'salePrice' does not exist on type 'Product'.
Property 'salePrice' does not exist on type 'RegularProduct'.
発生条件
- Union 型のオブジェクトで、一部の型にしか存在しないプロパティへアクセスした場合
- 型縮小の条件が不十分な場合
原因
TypeScript が「すべての型でプロパティが存在する保証がない」と判断するためです。
解決方法
inオペレータで型縮小を行う- 型ガード関数を使用する
修正前のコード(エラーが発生)です。
typescripttype RegularProduct = { id: string; price: number };
type SaleProduct = { id: string; salePrice: number };
type Product = RegularProduct | SaleProduct;
function displayPrice(product: Product) {
// ❌ エラー: Property 'salePrice' does not exist on type 'Product'
console.log(product.salePrice);
}
修正後のコード(正常動作)です。
typescriptfunction displayPrice(product: Product) {
if ("salePrice" in product) {
// ✅ SaleProduct 型に縮小
console.log(product.salePrice);
} else {
// RegularProduct 型として処理
console.log(product.price);
}
}
解決後の確認
修正後、TypeScript のコンパイルエラーが解消され、正常に動作することを確認しました。
参考リンク
エラー 2: Cannot read properties of undefined
発生状況
optional プロパティで in オペレータを使った場合に、実行時エラーが発生します。
arduinoTypeError: Cannot read properties of undefined (reading 'toFixed')
発生条件
- optional プロパティ(
?付き)に対してinオペレータのみで判定 - プロパティが存在しても値が
undefinedの場合
原因
in オペレータはプロパティの存在のみをチェックし、値が undefined かどうかは判定しないためです。
解決方法
inオペレータとundefinedチェックを併用する- アサーション関数で値の存在を保証する
修正前のコード(エラーが発生)です。
typescripttype UserProfile = {
name: string;
age?: number;
};
function printAge(profile: UserProfile) {
if ("age" in profile) {
// ❌ profile.age が undefined の可能性
console.log(profile.age.toFixed());
}
}
修正後のコード(正常動作)です。
typescriptfunction printAge(profile: UserProfile) {
if ("age" in profile && profile.age !== undefined) {
// ✅ profile.age は number 型
console.log(profile.age.toFixed());
}
}
解決後の確認
修正後、age プロパティが undefined の場合でも実行時エラーが発生しないことを確認しました。
参考リンク
エラー 3: Argument of type 'X' is not assignable to parameter of type 'Y'
発生状況
型ガード関数の実装が不正確な場合に発生します。
goerror TS2345: Argument of type 'unknown' is not assignable to parameter of type '{ id: number }'.
発生条件
- カスタム型ガード(
is)の条件が不十分 - 型ガード関数が
falseを返した後の型推論が不正確
原因
型ガード関数の実装ロジックが、実際の型判定条件と一致していないためです。
解決方法
- 型ガード関数の条件を詳細に記述する
- すべての必須プロパティの存在と型をチェックする
修正前のコード(エラーが発生)です。
typescriptinterface User {
id: number;
name: string;
}
// ❌ 不十分な型ガード
function isUser(value: unknown): value is User {
return typeof value === "object" && value !== null;
}
function processUser(data: unknown) {
if (isUser(data)) {
// data が User 型として扱われるが、実際には保証されていない
console.log(data.id, data.name);
}
}
修正後のコード(正常動作)です。
typescript// ✅ 詳細な型ガード
function isUser(value: unknown): value is User {
return (
typeof value === "object" &&
value !== null &&
"id" in value &&
typeof (value as any).id === "number" &&
"name" in value &&
typeof (value as any).name === "string"
);
}
function processUser(data: unknown) {
if (isUser(data)) {
// data は User 型として安全に扱える
console.log(data.id, data.name);
}
}
解決後の確認
修正後、型ガード関数が正確に型を判定し、コンパイルエラーが解消されました。
参考リンク
エラー 4: Expected 1 arguments, but got 0 (asserts 関数)
発生状況
アサーション関数を呼び忘れた場合に発生します。
goerror TS2554: Expected 1 arguments, but got 0.
発生条件
asserts関数を定義したが、実際に呼び出していない- 関数呼び出しの位置が不適切
原因
アサーション関数は、呼び出した時点で型縮小が発生するため、呼び出しを忘れると型縮小されません。
解決方法
- アサーション関数を適切な位置で呼び出す
- 型縮小が必要な箇所の直前で実行する
修正前のコード(エラーが発生)です。
typescriptfunction assertIsString(value: unknown): asserts value is string {
if (typeof value !== "string") {
throw new Error("Expected string");
}
}
function processValue(input: unknown) {
// ❌ アサーション関数を呼び忘れ
console.log(input.toUpperCase()); // エラー
}
修正後のコード(正常動作)です。
typescriptfunction processValue(input: unknown) {
// ✅ アサーション関数を呼び出し
assertIsString(input);
// この行以降、input は string 型
console.log(input.toUpperCase());
}
解決後の確認
修正後、型縮小が正常に機能し、コンパイルエラーが解消されました。
参考リンク
まとめ
TypeScript の型縮小は、型安全なコードを書くための重要な技術です。本記事では、実務でよく使う 4 つのパターンを早見表と具体例で整理しました。
各パターンの使い分け基準
inオペレータ: シンプルなプロパティ存在判定に最適。Union 型のオブジェクト処理で第一選択instanceofオペレータ: クラスベースの型判定に使用。継承関係がある設計で威力を発揮is型ガード: 複雑な条件や再利用が必要な場合に選択。柔軟性が高いが実装コストも高いassertsアサーション: 外部データのバリデーションに有効。型安全性とランタイムチェックを統合
向いているケース・向かないケース
in オペレータが向いているケース
- API レスポンスの success/error 判定
- Union 型のオブジェクトで特定プロパティの有無で判定できる場合
- シンプルで高速な型判定が必要な場合
in オペレータが向かないケース
- クラスの継承関係がある場合
- optional プロパティの厳密な判定
- 外部データの詳細なバリデーション
instanceof が向いているケース
- クラスベースの設計
- Date、Array などの組み込みオブジェクト判定
- 継承チェーンを辿る必要がある場合
instanceof が向かないケース
- プリミティブ型の判定
- インターフェースの判定
- クラス設計を採用していない場合
is 型ガードが向いているケース
- 複雑な条件ロジック
- 再利用可能な型チェック関数
- unknown や any からの型縮小
is 型ガードが向かないケース
- シンプルな判定(
inやinstanceofで十分な場合) - 一度しか使わない判定
- 実装コストをかけたくない場合
asserts が向いているケース
- 外部 API からのデータ検証
- ユーザー入力のバリデーション
- 不正データを早期に検出したい場合
asserts が向かないケース
- パフォーマンスが重要な場面(例外処理のコストが高い)
- 例外を投げたくない設計
- シンプルな型判定で十分な場合
私が実務で学んだ教訓
- シンプルな判定ではシンプルな手法を選ぶ: 過剰な実装は可読性を下げる
- 外部データには
assertsを使う: 型安全性とバリデーションを同時に確保 - 複数パターンを組み合わせる: 段階的な型縮小で堅牢なコードを実現
- optional プロパティには注意:
inだけでなくundefinedチェックも併用
型縮小をマスターすることで、TypeScript の型安全性を最大限に活かし、実行時エラーを減らせます。本記事の早見表を参考に、実務で適切なパターンを選択していただければ幸いです。
関連リンク
著書
article2026年1月10日TypeScriptでモノレポ管理をセットアップする手順 プロジェクト分割と依存関係制御の実践
article2026年1月10日StorybookとTypeScriptのユースケース 型安全なUI開発を設計して運用する
article2026年1月9日TypeScriptプロジェクトの整形とLintをセットアップする手順 PrettierとESLintの最適構成
article2026年1月9日Vue 3とTypeScriptをセットアップして型安全に始める手順 propsとemitsの設計も整理
article2026年1月9日TypeScriptのビルド最適化を比較・検証する esbuild swc tscのベンチマークと使い分け
article2026年1月8日ESLintのparser設定を比較・検証する Babel TypeScript Flowの違いと選び方
article2026年1月10日TypeScriptでモノレポ管理をセットアップする手順 プロジェクト分割と依存関係制御の実践
article2026年1月10日StorybookとTypeScriptのユースケース 型安全なUI開発を設計して運用する
article2026年1月9日TypeScriptプロジェクトの整形とLintをセットアップする手順 PrettierとESLintの最適構成
article2026年1月9日Vue 3とTypeScriptをセットアップして型安全に始める手順 propsとemitsの設計も整理
article2026年1月9日TypeScriptのビルド最適化を比較・検証する esbuild swc tscのベンチマークと使い分け
article2026年1月8日ESLintのparser設定を比較・検証する Babel TypeScript Flowの違いと選び方
blogiPhone 17シリーズの発表!全モデルiPhone 16から進化したポイントを見やすく整理
blogGoogleストアから訂正案内!Pixel 10ポイント有効期限「1年」表示は誤りだった
blog【2025年8月】Googleストア「ストアポイント」は1年表記はミス?2年ルールとの整合性を検証
blogGoogleストアの注文キャンセルはなぜ起きる?Pixel 10購入前に知るべき注意点
blogPixcel 10シリーズの発表!全モデル Pixcel 9 から進化したポイントを見やすく整理
blogフロントエンドエンジニアの成長戦略:コーチングで最速スキルアップする方法
review今の自分に満足していますか?『持たざる者の逆襲 まだ何者でもない君へ』溝口勇児
reviewついに語られた業界の裏側!『フジテレビの正体』堀江貴文が描くテレビ局の本当の姿
review愛する勇気を持てば人生が変わる!『幸せになる勇気』岸見一郎・古賀史健のアドラー実践編で真の幸福を手に入れる
review週末を変えれば年収も変わる!『世界の一流は「休日」に何をしているのか』越川慎司の一流週末メソッド
review新しい自分に会いに行こう!『自分の変え方』村岡大樹の認知科学コーチングで人生リセット
review科学革命から AI 時代へ!『サピエンス全史 下巻』ユヴァル・ノア・ハラリが予見する人類の未来
