TypeScript Type Guard の実務活用法|型安全なコード設計パターン
TypeScript を使う上で避けて通れない「型安全性」の問題。特に API レスポンスやユーザー入力を扱う際、実行時に予期しない型が来ることがあります。そこで活躍するのが Type Guard です。本記事では、教科書的な例ではなく、実務で本当に使われるパターンを中心に解説します。
Type Guard とは|簡易的な解説
Type Guard(型ガード)は、TypeScript のコンパイラに「この値はこの型だ」と保証する機能です。実行時に型を検査し、その結果に基づいて型を絞り込みます。
基本的には以下のような形式です:
function isString(value: unknown): value is string {
return typeof value === 'string';
}
const result: unknown = getValue();
if (isString(result)) {
// ここでは result は string 型として扱える
console.log(result.toUpperCase());
}
では、実務ではどのような場面で必要になるのでしょうか?
業務でのユースケース
1. API レスポンスの検証
REST API から返ってくるデータは、常に期待した形とは限りません。特に外部 API や古いバージョンとの互換性を保つ場合、実行時の型検証は必須です。
2. ユーザー入力のバリデーション
フォームやファイルアップロードからのデータは、すべて unknown 型で扱う必要があります。type guard で安全に型変換を行います。
3. ポリモーフィズム処理
複数の型が混在する配列を処理する際、type guard で型を判定しながら処理を分岐させます。
4. エラーハンドリング
catch ブロックで受け取る error オブジェクトは any 型です。type guard で安全にエラー情報にアクセスします。
実装コード|実務パターン
パターン 1: API レスポンスの検証
実際のプロジェクトでよくあるシナリオです。ユーザーデータを取得する API があり、リソース型が複数存在する場合:
// レスポンスの型定義
interface UserResponse {
id: number;
name: string;
email: string;
role: 'admin' | 'user';
}
interface ErrorResponse {
error: string;
status: number;
timestamp: string;
}
// Type Guard の実装
function isUserResponse(value: unknown): value is UserResponse {
if (typeof value !== 'object' || value === null) return false;
const obj = value as Record;
return (
typeof obj.id === 'number' &&
typeof obj.name === 'string' &&
typeof obj.email === 'string' &&
(obj.role === 'admin' || obj.role === 'user')
);
}
function isErrorResponse(value: unknown): value is ErrorResponse {
if (typeof value !== 'object' || value === null) return false;
const obj = value as Record;
return (
typeof obj.error === 'string' &&
typeof obj.status === 'number' &&
typeof obj.timestamp === 'string'
);
}
// 実際の使用例
async function fetchUser(userId: number) {
try {
const response = await fetch(`/api/users/${userId}`);
const data = await response.json();
if (isUserResponse(data)) {
// ここで data は UserResponse 型として扱える
console.log(`ユーザー ${data.name} を取得しました`);
return data;
} else if (isErrorResponse(data)) {
console.error(`エラー [${data.status}]: ${data.error}`);
throw new Error(data.error);
} else {
// 予期した形式のデータが来た場合
console.error('不正なレスポンス形式です', data);
throw new Error('予期しないレスポンス形式');
}
} catch (error) {
console.error('API 呼び出しに失敗しました', error);
throw error;
}
}
パターン 2: 複数型を含む配列の処理
EC サイトで、商品と記事が混在するリスト処理の例です:
// 型定義
interface Product {
type: 'product';
id: number;
name: string;
price: number;
stock: number;
}
interface Article {
type: 'article';
id: number;
title: string;
content: string;
publishedAt: string;
}
type ContentItem = Product | Article;
// Type Guard の実装(discriminator pattern を活用)
function isProduct(item: ContentItem): item is Product {
return item.type === 'product';
}
function isArticle(item: ContentItem): item is Article {
return item.type === 'article';
}
// より詳細な検証が必要な場合
function isValidProduct(value: unknown): value is Product {
if (typeof value !== 'object' || value === null) return false;
const item = value as Record;
return (
item.type === 'product' &&
typeof item.id === 'number' &&
typeof item.name === 'string' &&
typeof item.price === 'number' &&
typeof item.stock === 'number' &&
item.price > 0 &&
item.stock >= 0
);
}
// 実装例
function processContentList(items: ContentItem[]) {
items.forEach(item => {
if (isProduct(item)) {
// 商品の処理
const discount = item.price * 0.1;
console.log(`${item.name}: ¥${item.price} (在庫: ${item.stock})`);
} else if (isArticle(item)) {
// 記事の処理
const date = new Date(item.publishedAt);
console.log(`【記事】${item.title} (${date.toLocaleDateString()})`);
}
});
}
パターン 3: エラーハンドリング
API 呼び出しやファイル操作でのエラー処理は、type guard なしでは危険です:
// カスタムエラー型の定義
interface ApiError {
code: string;
message: string;
details?: Record;
}
interface ValidationError {
field: string;
message: string;
value: unknown;
}
// Type Guard の実装
function isApiError(error: unknown): error is ApiError {
if (typeof error !== 'object' || error === null) return false;
const err = error as Record;
return (
typeof err.code === 'string' &&
typeof err.message === 'string'
);
}
function isValidationError(error: unknown): error is ValidationError {
if (typeof error !== 'object' || error === null) return false;
const err = error as Record;
return (
typeof err.field === 'string' &&
typeof err.message === 'string'
);
}
function isError(value: unknown): value is Error {
return value instanceof Error;
}
// 使用例
async function safeApiCall() {
try {
await someApiCall();
} catch (error) {
if (isApiError(error)) {
console.error(`API エラー [${error.code}]: ${error.message}`);
if (error.details) {
console.error('詳細情報:', error.details);
}
} else if (isValidationError(error)) {
console.error(`バリデーションエラー [${error.field}]: ${error.message}`);
} else if (isError(error)) {
console.error(`エラー: ${error.message}`);
} else {
console.error('不明なエラーが発生しました:', error);
}
}
}
パターン 4: フォーム送信データの検証
React や Vue でフォームデータを送信する際の実装例:
// フォームデータ型の定義
interface RegistrationForm {
username: string;
email: string;
password: string;
age: number;
terms_accepted: boolean;
}
// 詳細な検証を含む Type Guard
function isValidRegistrationForm(data: unknown): data is RegistrationForm {
if (typeof data !== 'object' || data === null) return false;
const form = data as Record;
// 基本的な型チェック
if (typeof form.username !== 'string') return false;
if (typeof form.email !== 'string') return false;
if (typeof form.password !== 'string') return false;
if (typeof form.age !== 'number') return false;
if (typeof form.terms_accepted !== 'boolean') return false;
// ビジネスロジックに基づく検証
if (form.username.length < 3 || form.username.length > 20) return false;
if (!form.email.includes('@')) return false;
if (form.password.length < 8) return false;
if (form.age < 13 || form.age > 120) return false;
if (!form.terms_accepted) return false;
return true;
}
// 使用例
async function handleFormSubmit(formData: unknown) {
if (!isValidRegistrationForm(formData)) {
console.error('フォームデータが不正です');
return;
}
// formData は RegistrationForm 型として安全に使用できる
try {
const response = await fetch('/api/register', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(formData)
});
const result = await response.json();
console.log('登録成功:', result);
} catch (error) {
console.error('登録処理に失敗しました:', error);
}
}
よくある応用パターン
型述語関数の再利用と組み合わせ
複数の type guard を組み合わせて、より複雑な検証を行うことがあります:
// 基本的な type guard
function isString(value: unknown): value is string {
return typeof value === 'string';
}
function isNumber(value: unknown): value is number {
return typeof value === 'number';
}
function isArray(value: unknown): value is unknown[] {
return Array.isArray(value);
}
// より高度な検証
function isStringArray(value: unknown): value is string[] {
return isArray(value) && value.every(item => isString(item));
}
function isNumberArray(value: unknown): value is number[] {
return isArray(value) && value.every(item => isNumber(item));
}
// 使用例
const data = getUserInput();
if (isStringArray(data)) {
const tags = data.map(tag => tag.toUpperCase());
console.log('タグ一覧:', tags);
} else if (isNumberArray(data)) {
const total = data.reduce((sum, num) => sum + num, 0);
console.log('合計:', total);
}
Generic を使った汎用的な type guard
複数の API から異なる形式のレスポンスを取得する場合、汎用的な検証が便利です:
// 汎用的な API レスポンス検証
interface ApiResponse {
success: boolean;
data?: T;
error?: string;
}
function isApiResponse(
value: unknown,
isValidData: (data: unknown) => data is T
): value is ApiResponse {
if (typeof value !== 'object' || value === null) return false;
const response = value as Record;
if (typeof response.success !== 'boolean') return false;
if (response.success) {
return isValidData(response.data);
}
return typeof response.error === 'string' || response.error === undefined;
}
// 使用例
interface UserData {
id: number;
name: string;
}
function isUserData(value: unknown): value is UserData {
if (typeof value !== 'object' || value === null) return false;
const obj = value as Record;
return typeof obj.id === 'number' && typeof obj.name === 'string';
}
async function fetchUserData(userId: number) {
const response = await fetch(`/api/user/${userId}`);
const data = await response.json();
if (isApiResponse(data, isUserData)) {
if (data.success && data.data) {
console.log(`ユーザー: ${data.data.name}`);
} else {
console.error('ユーザーデータの取得に失敗しました');
}
} else {
console.error('予期しないレスポンス形式です');
}
}
注意点と実務での工夫
1. 型ガードの漏れを防ぐ
exhaustive check パターンで、すべての型を網羅していることを確認します:
type Status = 'pending' | 'approved' | 'rejected';
function getStatusLabel(status: unknown): string {
if (status === 'pending') return '審査中';
if (status === 'approved') return '承認';
if (status === 'rejected') return '却下';
// 新しいステータスが追加されると、ここでコンパイルエラーになる
const exhaustiveCheck: never = status;
return exhaustiveCheck;
}
2. 過度な防御的プログラミングを避ける
すべてを unknown 型で扱う必要はありません。信頼できるデータソースに対しては、型アサーションで十分な場合もあります。ただし、API レスポンスやユーザー入力には type guard を使いましょう。
3. パフォーマンスへの配慮
type guard で毎回すべてのプロパティをチェックしていると、処理が重くなる可能性があります。特に大量データの場合、チェック項目を最小限にする工夫が必要です:
// 簡易版(パフォーマンス重視)
function isProductFast(item: unknown): item is Product {
return typeof item === 'object' && item !== null && 'type' in item && item.type === 'product';
}
// 詳細版(安全性重視)
function isProductFull(item: unknown): item is Product {
if (typeof item !== 'object' || item === null) return false;
const p = item as Record;
return (
p.type === 'product' &&
typeof p.id === 'number' &&
typeof p.name === 'string' &&
typeof p.price === 'number' &&
p.price > 0
);
}
4. テストの重要性
type guard は実行時の検証なので、テストケースで確実に動作することを確認する必要があります:
// Jest を使ったテスト例
describe('Type Guards', () => {
describe('isUserResponse', () => {
it('正常なユーザーレスポンスを判定できる', () => {
const validData = {
id: 1,
name: 'John',
email: 'john@example.com',
role: 'admin'
};
expect(isUserResponse(validData)).toBe(true);
});
it('無効なメールを持つレスポンスを却下できる', () => {
const invalidData = {
id: 1,
name: 'John',
email: 'invalid-email',
role: 'admin'
};
expect(isUserResponse(invalidData)).toBe(false);
});
it('null を却下できる', () => {
expect(isUserResponse(null)).toBe(false);
});
it('未定義の role を却下できる', () => {
const invalidData = {
id: 1,
name: 'John',
email: 'john@example.com',
role: 'superuser'
};
expect(isUserResponse(invalidData)).toBe(false);
});
});
});
実務でのベストプラクティス
1. Type Guard を集中管理する
プロジェクトが大きくなると、type guard の数も増えます。専用のファイルで管理するのが便利です:
// guards.ts
export function isUserResponse(value: unknown): value is UserResponse {
// 実装
}
export function isProductResponse(value: unknown): value is ProductResponse {
// 実装
}
export function isApiError(error: unknown): error is ApiError {
// 実装
}
// api.ts でインポートして使用
import { isUserResponse, isApiError } from './guards';
async function fetchData() {
// 実装
}
2. Zod や io-ts などの検証ライブラリの活用
複雑なスキーマ検証が必要な場合、Zod などのライブラリを使うと実装が簡潔になります:
import { z } from 'zod';
const UserSchema = z.object({
id: z.number(),
name: z.string().min(1),
email: z.string().email(),
role: z.enum(['admin', 'user'])
});
type User = z.infer;
async function fetchUser(userId: number) {
const response = await fetch(`/api/users/${userId}`);
const data = await response.json();
try {
const user = UserSchema.parse(data);
console.log('ユーザーデータ:', user);
} catch (error) {
console.error('バリデーションエラー:', error);
}
}
まとめ
TypeScript の type guard は、実行時の型安全性を確保するための実務的で重要な機能です。特に API レスポンスやユーザー入力を扱う場合、type guard なしで開発することは危険です。
実務での使い方をまとめると:
- API レスポンス:isUserResponse のような明確な型ガードを実装する
- 複合型の処理:discriminator pattern で効率的に型を判定する
- エラーハンドリング:catch ブロックの error を安全に扱う
- フォーム検証:ビジネスロジックを含めた詳細な検証を実装する
- テスト:type guard の動作を確実に確認する
- 複雑な場合:Zod などの検証ライブラリを活用する
Type guard の適切な使用は、バグの予防だけでなく、コードの可読性と保守性も向上させます。これからの TypeScript 開発では、ぜひ type guard を活用してください。

