✨
TypeScriptのUnion Type活用術:APIレスポンスで学ぶ実践的な型設計
はじめに
フロントエンド開発において、TypeScriptのUnion Typeは強力な型安全性を提供する重要な機能です。特にAPIレスポンスの型定義では、Union Typeを使うかOptional Propertiesを使うかで、実装が変わります。
具体例:ユーザー検索APIのレスポンス
以下のようなユーザー検索APIがあるとします:
// 成功時のレスポンス
{
"status": "success",
"data": {
"users": [
{
"id": "123",
"name": "田中太郎",
"email": "tanaka@example.com"
}
],
"totalCount": 10,
"hasMore": true
}
}
// エラー時のレスポンス
{
"status": "error",
"error": {
"code": "INVALID_QUERY",
"message": "検索クエリが無効です",
"details": {
"field": "name",
"reason": "文字数が不足しています"
}
}
}
// ロード中のレスポンス(フロントエンドの状態管理用)
{
"status": "loading"
}
このAPIレスポンスに対して、Union TypeとOptional Propertiesの2つの異なるアプローチで型を定義してみましょう。
アプローチ1:Union Type(厳格な型定義)
// 厳格な型定義
type ApiResponse = {
status: 'success';
data: {
users: User[];
totalCount: number;
hasMore: boolean;
};
} | {
status: 'error';
error: {
code: string;
message: string;
details?: {
field: string;
reason: string;
};
};
} | {
status: 'loading';
};
type User = {
id: string;
name: string;
email: string;
};
// 実装例
function handleResponse(response: ApiResponse) {
switch (response.status) {
case 'success':
// TypeScriptがresponse.dataの存在を保証
console.log(`${response.data.users.length}件のユーザーが見つかりました`);
response.data.users.forEach(user => {
console.log(`${user.name} (${user.email})`);
});
break;
case 'error':
// TypeScriptがresponse.errorの存在を保証
console.error(`エラー: ${response.error.message}`);
if (response.error.details) {
console.error(`詳細: ${response.error.details.reason}`);
}
break;
case 'loading':
// loadingの場合は追加のプロパティなし
console.log('読み込み中...');
break;
}
}
アプローチ2:Optional Properties(緩やかな型定義)
// 緩やかな型定義
interface ApiResponse {
status: 'success' | 'error' | 'loading';
data?: {
users: User[];
totalCount: number;
hasMore: boolean;
};
error?: {
code: string;
message: string;
details?: {
field: string;
reason: string;
};
};
}
type User = {
id: string;
name: string;
email: string;
};
// 実装例
function handleResponse(response: ApiResponse) {
switch (response.status) {
case 'success':
// 実行時チェックが必要
if (response.data) {
console.log(`${response.data.users.length}件のユーザーが見つかりました`);
response.data.users.forEach(user => {
console.log(`${user.name} (${user.email})`);
});
}
break;
case 'error':
// 実行時チェックが必要
if (response.error) {
console.error(`エラー: ${response.error.message}`);
if (response.error.details) {
console.error(`詳細: ${response.error.details.reason}`);
}
}
break;
case 'loading':
console.log('読み込み中...');
break;
}
}
Union Type vs Optional Properties:メリット・デメリットの比較
Union Typeのメリット
1. 型安全性の向上
// ✅ コンパイル時にエラーを検出
function processSuccess(response: ApiResponse) {
if (response.status === 'success') {
// response.dataが確実に存在することをTypeScriptが保証
return response.data.users.map(user => user.name);
}
}
2. 不正なデータ構造の防止
// ❌ このような不正な組み合わせはコンパイルエラーになる
const invalidResponse: ApiResponse = {
status: 'success',
error: { code: 'ERR', message: 'error' } // エラー: successにerrorは存在しない
};
3. IDEのサポートが充実
- 自動補完が正確
- リファクタリング時の変更箇所を正確に検出
- 未使用プロパティの警告
Union Typeのデメリット
1. 型定義が複雑になる
// 新しいステータスを追加する場合、Union Typeを拡張する必要がある
type ApiResponse = {
status: 'success';
data: SuccessData;
} | {
status: 'error';
error: ErrorData;
} | {
status: 'loading';
} | {
status: 'partial'; // 新しいステータス
data: PartialData;
warnings: Warning[];
};
2. 共通プロパティの重複
// statusやtimestampなどの共通プロパティが重複する
type ApiResponse = {
status: 'success';
timestamp: string; // 重複
requestId: string; // 重複
data: SuccessData;
} | {
status: 'error';
timestamp: string; // 重複
requestId: string; // 重複
error: ErrorData;
};
Optional Propertiesのメリット
1. シンプルで理解しやすい
// 一目で全体の構造が把握できる
interface ApiResponse {
status: 'success' | 'error' | 'loading';
data?: SuccessData;
error?: ErrorData;
timestamp?: string;
requestId?: string;
}
2. 拡張が容易
// 新しいプロパティの追加が簡単
interface ApiResponse {
status: 'success' | 'error' | 'loading' | 'partial';
data?: SuccessData;
error?: ErrorData;
warnings?: Warning[]; // 簡単に追加できる
metadata?: Metadata; // 簡単に追加できる
}
3. 外部APIとの統合が楽
// 外部APIの仕様変更に柔軟に対応
interface ThirdPartyApiResponse {
status: string; // 予期しない値も受け入れ可能
data?: any;
error?: any;
[key: string]: any; // 追加のプロパティも許容
}
Optional Propertiesのデメリット
1. Optional Chainingが常に必要
// ❌ 毎回 ?. を付けなければならない
function processResponse(response: ApiResponse) {
console.log(response.data?.users?.length); // ?. が必要
console.log(response.data?.totalCount); // ?. が必要
console.log(response.error?.message); // ?. が必要
// 配列の操作でも常に存在チェックが必要
response.data?.users?.forEach(user => {
console.log(user.name);
});
}
2. ランタイムエラーのリスク
// ❌ Optional Chainingを忘れると実行時にクラッシュ
function processResponse(response: ApiResponse) {
if (response.status === 'success') {
// response.dataがundefinedの場合、実行時エラー
return response.data.users.length; // TypeError: Cannot read property 'users' of undefined
}
}
2. 不正なデータの組み合わせを許容
// ❌ 論理的におかしい組み合わせもコンパイルが通ってしまう
const invalidResponse: ApiResponse = {
status: 'success',
error: { code: 'ERR', message: 'error' } // 論理的には矛盾
};
3. 防御的プログラミングが必要
// 常に存在チェックが必要
function handleResponse(response: ApiResponse) {
if (response.status === 'success' && response.data) {
// 二重チェックが必要
if (response.data.users && Array.isArray(response.data.users)) {
// さらに配列の型チェックも必要
response.data.users.forEach(user => {
if (user && user.name && user.email) {
console.log(`${user.name} (${user.email})`);
}
});
}
}
}
コードの書きやすさの比較
Union Typeでの実装
// ✅ シンプルで直感的
function displayUsers(response: ApiResponse) {
switch (response.status) {
case 'success':
// ?. 不要!TypeScriptがdataの存在を保証
console.log(`総件数: ${response.data.totalCount}`);
response.data.users.forEach(user => {
console.log(`${user.name} (${user.email})`);
});
break;
case 'error':
// ?. 不要!TypeScriptがerrorの存在を保証
console.error(response.error.message);
break;
}
}
Optional Propertiesでの実装
// ❌ 毎回 Optional Chaining や 存在チェックが必要
function displayUsers(response: ApiResponse) {
switch (response.status) {
case 'success':
// 毎回 ?. を付ける必要がある
console.log(`総件数: ${response.data?.totalCount ?? 0}`);
response.data?.users?.forEach(user => {
console.log(`${user.name} (${user.email})`);
});
break;
case 'error':
// エラーメッセージも ?. が必要
console.error(response.error?.message ?? 'Unknown error');
break;
}
}
// または、より安全な書き方
function displayUsersWithGuard(response: ApiResponse) {
if (response.status === 'success' && response.data) {
console.log(`総件数: ${response.data.totalCount}`);
response.data.users.forEach(user => {
console.log(`${user.name} (${user.email})`);
});
} else if (response.status === 'error' && response.error) {
console.error(response.error.message);
}
}
Reactでの使用例
// Union Type
function UserList({ response }: { response: ApiResponse }) {
switch (response.status) {
case 'success':
return (
<div>
<p>総件数: {response.data.totalCount}</p>
{response.data.users.map(user => (
<div key={user.id}>{user.name}</div>
))}
</div>
);
case 'error':
return <div>エラー: {response.error.message}</div>;
case 'loading':
return <div>読み込み中...</div>;
}
}
// 緩やかな型定義
function UserList({ response }: { response: ApiResponse }) {
if (response.status === 'success' && response.data) {
return (
<div>
<p>総件数: {response.data.totalCount}</p>
{response.data.users?.map(user => (
<div key={user.id}>{user.name}</div>
))}
</div>
);
}
if (response.status === 'error') {
return <div>エラー: {response.error?.message ?? 'Unknown error'}</div>;
}
return <div>読み込み中...</div>;
}
実践的な選択指針
Union Typeを選ぶべき場合
1. 内部API(自社開発)
- 仕様が明確に定まっている
- レスポンス構造の変更をコントロールできる
- 長期的な保守性を重視する
2. 重要なビジネスロジック
- データの整合性が重要
- バグによる影響が大きい
3. 大規模なチーム開発
- 複数の開発者が同じコードを触る
- コードレビューでのミス発見を重視
- 型による自動テストを活用したい
Optional Propertiesを選ぶべき場合
1. 外部API(サードパーティ)
- 仕様変更をコントロールできない
- ドキュメントが不完全な場合がある
- レスポンス構造が頻繁に変わる可能性
2. プロトタイプ開発
- 要件が流動的
- 開発速度を重視
- 後でリファクタリング予定
3. レガシーシステムとの連携
- データ構造が複雑で予測困難
- 段階的な移行が必要
- 既存システムの制約が多い
実際のプロジェクトでの使い分け例
// 内部API: Union Type
type InternalApiResponse = {
status: 'success';
data: UserData;
} | {
status: 'error';
error: ApiError;
};
// 外部API: Optional Properties
interface ExternalApiResponse {
status?: string;
data?: any;
error?: any;
[key: string]: any;
}
// ハイブリッド: 基本構造はUnion Type、詳細はOptional
type HybridApiResponse = {
status: 'success';
data: {
users: User[];
metadata?: Record<string, any>; // 将来の拡張に備える
};
} | {
status: 'error';
error: {
code: string;
message: string;
[key: string]: any; // 詳細なエラー情報は柔軟に
};
};
まとめ
APIレスポンスの型定義において、厳格さと緩やかさの選択は、プロジェクトの性質と開発フェーズに大きく依存します。
重要なポイント:
- 内部APIは厳格に、外部APIは緩やかに
- プロダクション環境では厳格に、プロトタイプでは緩やかに
- チーム開発では厳格に、個人開発では状況に応じて
適切な型設計により、開発効率と品質のバランスを取りながら、保守しやすいコードベースを構築できます。
Discussion