TypeScript Intersection Type(交差型)の実務活用ガイド|実装パターンと注意点

TypeScript

TypeScript Intersection Type(交差型)の実務活用ガイド|実装パターンと注意点

簡易的な解説

TypeScriptのIntersection Type(交差型)は、複数の型を組み合わせて新しい型を作成する機能です。記号は「&」を使用します。Union Type(|)が「AまたはB」という意味に対して、Intersection Typeは「AかつB」という意味を持ちます。

基本的な構文は以下の通りです:

type Combined = TypeA & TypeB;

これにより、Combined型は TypeAとTypeBの両方のプロパティを持つ必要があります。

業務でのユースケース

Intersection Typeが活躍する場面は数多くあります。実務では以下のようなシーンで頻繁に登場します:

  • 複数のAPIからのレスポンスを統合する:ユーザー情報とプロフィール情報を合わせたデータ型
  • 認証情報と業務データの結合:ログインユーザーの権限情報と操作対象のリソースを組み合わせる
  • ミドルウェアのチェーン処理:リクエストオブジェクトに複数のミドルウェアが情報を追加していく場面
  • プラグインアーキテクチャ:ベースとなるオブジェクトに複数の機能を拡張する
  • フォーム入力値の型安全性:複数の検証結果を組み合わせたメタデータ

実装コード①:APIレスポンスの統合

最初の例として、実務でよくある「複数のAPIからデータを取得して統合する」というシーンを見てみましょう。

// ユーザー管理API から取得するデータ型
type UserBase = {
  id: string;
  email: string;
  name: string;
  createdAt: Date;
};

// プロフィールAPI から取得するデータ型
type UserProfile = {
  bio: string;
  avatarUrl: string;
  company: string;
  location: string;
};

// 権限管理API から取得するデータ型
type UserPermissions = {
  role: 'admin' | 'moderator' | 'user';
  permissions: string[];
  canDeleteUsers: boolean;
  canEditContent: boolean;
};

// 複数のAPIデータを統合した型
type CompleteUser = UserBase & UserProfile & UserPermissions;

// 実際の使用例
async function fetchCompleteUserData(userId: string): Promise {
  const [userBase, profile, permissions] = await Promise.all([
    fetch(`/api/users/${userId}`).then(r => r.json()),
    fetch(`/api/users/${userId}/profile`).then(r => r.json()),
    fetch(`/api/users/${userId}/permissions`).then(r => r.json())
  ]);

  // 複数のレスポンスを組み合わせて、CompleteUser型として返す
  const completeUser: CompleteUser = {
    ...userBase,
    ...profile,
    ...permissions
  };

  return completeUser;
}

// 型安全に使用できる
const user = await fetchCompleteUserData('user-123');
console.log(user.email);           // UserBase から
console.log(user.bio);             // UserProfile から
console.log(user.permissions);     // UserPermissions から

このパターンでは、3つの異なるAPIから取得したデータを1つの型で管理できます。TypeScriptの型チェッカーが全てのプロパティの存在を保証してくれるため、実行時エラーを大幅に削減できます。

実装コード②:ミドルウェアチェーンでの値の拡張

Express.jsなどのフレームワークを使用している場合、リクエストオブジェクトに対して複数のミドルウェアが情報を追加していくシーンがあります。

// 基本的なリクエスト型
type BaseRequest = {
  method: 'GET' | 'POST' | 'PUT' | 'DELETE';
  path: string;
  body?: Record;
};

// 認証ミドルウェアが追加する情報
type AuthenticatedRequest = {
  userId: string;
  email: string;
  role: 'admin' | 'user';
};

// レート制限ミドルウェアが追加する情報
type RateLimitedRequest = {
  remainingRequests: number;
  resetAt: Date;
};

// ログミドルウェアが追加する情報
type LoggedRequest = {
  requestId: string;
  timestamp: Date;
  userAgent: string;
};

// 全てのミドルウェアを通過したリクエスト型
type FullyProcessedRequest = BaseRequest & AuthenticatedRequest & RateLimitedRequest & LoggedRequest;

// 実装例
function authMiddleware(req: BaseRequest): BaseRequest & AuthenticatedRequest {
  // 認証処理
  return {
    ...req,
    userId: 'user-123',
    email: 'user@example.com',
    role: 'user'
  };
}

function rateLimitMiddleware(req: BaseRequest & AuthenticatedRequest): BaseRequest & AuthenticatedRequest & RateLimitedRequest {
  // レート制限チェック
  return {
    ...req,
    remainingRequests: 95,
    resetAt: new Date(Date.now() + 3600000)
  };
}

function logMiddleware(req: BaseRequest & AuthenticatedRequest & RateLimitedRequest): FullyProcessedRequest {
  // ロギング処理
  return {
    ...req,
    requestId: `req-${Date.now()}`,
    timestamp: new Date(),
    userAgent: 'Mozilla/5.0...'
  };
}

// ミドルウェアチェーン
function processRequest(baseReq: BaseRequest): FullyProcessedRequest {
  let req = baseReq;
  req = authMiddleware(req);
  req = rateLimitMiddleware(req);
  req = logMiddleware(req);
  return req;
}

// 使用例
const processed = processRequest({
  method: 'POST',
  path: '/api/data',
  body: { key: 'value' }
});

// 全ての情報にアクセスできる
console.log(processed.userId);           // 認証情報
console.log(processed.remainingRequests); // レート制限
console.log(processed.requestId);         // ログ情報

実装コード③:権限管理システムの実装

実務でよくあるのが、権限管理と業務ロジックの組み合わせです。以下は、ユーザーのアクションに対して権限情報を含める例です。

// 基本的なアクション情報
type ActionBase = {
  id: string;
  actionType: 'create' | 'read' | 'update' | 'delete';
  resourceId: string;
  timestamp: Date;
};

// 実行者の情報
type ActorInfo = {
  userId: string;
  userName: string;
  role: 'admin' | 'moderator' | 'user';
};

// 権限チェック結果
type PermissionCheck = {
  isAllowed: boolean;
  denialReason?: string;
  auditLog: string;
};

// 監査ログ情報
type AuditInfo = {
  ipAddress: string;
  sessionId: string;
  riskScore: number;
};

// 完全なアクション記録
type AuditedAction = ActionBase & ActorInfo & PermissionCheck & AuditInfo;

// 権限チェック関数
function checkPermission(action: ActionBase & ActorInfo): PermissionCheck {
  const { actionType, role } = action;
  
  const permissions = {
    admin: ['create', 'read', 'update', 'delete'],
    moderator: ['read', 'update'],
    user: ['read']
  };

  const allowedActions = permissions[role];
  const isAllowed = allowedActions.includes(actionType);

  return {
    isAllowed,
    denialReason: isAllowed ? undefined : `Role ${role} cannot ${actionType}`,
    auditLog: `Action ${action.actionType} on resource ${action.resourceId} by ${action.userId}`
  };
}

// 監査ログ作成関数
function createAuditedAction(
  action: ActionBase & ActorInfo & PermissionCheck,
  ipAddress: string,
  sessionId: string
): AuditedAction {
  // リスクスコアの計算ロジック
  let riskScore = 0;
  if (action.role === 'user' && action.actionType === 'delete') riskScore += 50;
  if (!action.isAllowed) riskScore += 100;
  
  return {
    ...action,
    ipAddress,
    sessionId,
    riskScore
  };
}

// 使用例
const baseAction: ActionBase = {
  id: 'act-001',
  actionType: 'delete',
  resourceId: 'doc-123',
  timestamp: new Date()
};

const actorInfo: ActorInfo = {
  userId: 'user-456',
  userName: '山田太郎',
  role: 'user'
};

const actionWithActor = { ...baseAction, ...actorInfo };
const permissionCheck = checkPermission(actionWithActor);
const actionWithPermission = { ...actionWithActor, ...permissionCheck };

const fullAudit = createAuditedAction(
  actionWithPermission,
  '192.168.1.1',
  'sess-789'
);

// 結果確認
if (fullAudit.isAllowed) {
  console.log(`Action allowed: ${fullAudit.auditLog}`);
} else {
  console.log(`Action denied: ${fullAudit.denialReason}`);
}
console.log(`Risk score: ${fullAudit.riskScore}`);

実装コード④:フォーム検証の型安全な実装

フォーム入力値に対して複数の検証を行い、その結果を型で管理する例です。

// 入力フォームの基本情報
type FormInput = {
  email: string;
  password: string;
  confirmPassword: string;
};

// メールバリデーション結果
type EmailValidation = {
  isEmailValid: boolean;
  emailError?: string;
};

// パスワードバリデーション結果
type PasswordValidation = {
  isPasswordStrong: boolean;
  passwordError?: string;
  requirements: {
    hasUpperCase: boolean;
    hasNumber: boolean;
    hasLength8: boolean;
  };
};

// パスワード一致チェック結果
type PasswordMatchValidation = {
  passwordsMatch: boolean;
  matchError?: string;
};

// 完全なバリデーション結果
type FormValidationResult = FormInput & EmailValidation & PasswordValidation & PasswordMatchValidation;

// 検証関数群
function validateEmail(email: string): EmailValidation {
  const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
  const isEmailValid = emailRegex.test(email);
  
  return {
    isEmailValid,
    emailError: isEmailValid ? undefined : '有効なメールアドレスを入力してください'
  };
}

function validatePassword(password: string): PasswordValidation {
  const hasUpperCase = /[A-Z]/.test(password);
  const hasNumber = /[0-9]/.test(password);
  const hasLength8 = password.length >= 8;
  
  const isPasswordStrong = hasUpperCase && hasNumber && hasLength8;
  
  return {
    isPasswordStrong,
    passwordError: isPasswordStrong ? undefined : 'パスワードは8文字以上で、大文字と数字を含む必要があります',
    requirements: {
      hasUpperCase,
      hasNumber,
      hasLength8
    }
  };
}

function validatePasswordMatch(password: string, confirmPassword: string): PasswordMatchValidation {
  const passwordsMatch = password === confirmPassword;
  
  return {
    passwordsMatch,
    matchError: passwordsMatch ? undefined : 'パスワードが一致しません'
  };
}

// 統合検証関数
function validateForm(input: FormInput): FormValidationResult {
  const emailValidation = validateEmail(input.email);
  const passwordValidation = validatePassword(input.password);
  const matchValidation = validatePasswordMatch(input.password, input.confirmPassword);
  
  return {
    ...input,
    ...emailValidation,
    ...passwordValidation,
    ...matchValidation
  };
}

// 検証状態の判定
function isFormValid(result: FormValidationResult): boolean {
  return result.isEmailValid && result.isPasswordStrong && result.passwordsMatch;
}

// 使用例
const userInput: FormInput = {
  email: 'user@example.com',
  password: 'SecurePass123',
  confirmPassword: 'SecurePass123'
};

const validationResult = validateForm(userInput);

if (isFormValid(validationResult)) {
  console.log('フォーム送信可能');
  // サーバーへ送信
} else {
  console.log('バリデーションエラー');
  if (!validationResult.isEmailValid) {
    console.log(`メール: ${validationResult.emailError}`);
  }
  if (!validationResult.isPasswordStrong) {
    console.log(`パスワード: ${validationResult.passwordError}`);
  }
  if (!validationResult.passwordsMatch) {
    console.log(`一致: ${validationResult.matchError}`);
  }
}

よくある応用パターン

パターン①:型ガード関数との組み合わせ

Intersection Typeは型ガード関数と組み合わせると、より強力な型安全性を実現できます。

type User = {
  id: string;
  name: string;
};

type Admin = {
  adminId: string;
  permissions: string[];
};

type AdminUser = User & Admin;

// 型ガード関数
function isAdminUser(user: User): user is AdminUser {
  return 'adminId' in user && 'permissions' in user;
}

// 使用例
function handleUser(user: User) {
  if (isAdminUser(user)) {
    // ここで user は AdminUser 型として扱われる
    console.log(`Admin ${user.adminId} has permissions:`, user.permissions);
  } else {
    console.log(`Regular user ${user.id}`);
  }
}

パターン②:ジェネリックス との組み合わせ

Intersection Typeとジェネリクスを組み合わせると、再利用可能で柔軟な型が作れます。

// データソース共通情報
type DataSource = {
  id: string;
  source: 'database' | 'cache' | 'api';
  fetchedAt: Date;
};

// キャッシュメタデータ
type CacheMetadata = {
  ttl: number;
  isCached: boolean;
};

// 汎用的な型ビルダー
type DataWithMetadata = T & DataSource & CacheMetadata;

// 実際の使用
type UserData = {
  id: string;
  name: string;
  email: string;
};

type CachedUser = DataWithMetadata;

function fetchUser(id: string): CachedUser {
  return {
    id,
    name: '田中太郎',
    email: 'tanaka@example.com',
    source: 'cache',
    fetchedAt: new Date(),
    ttl: 3600,
    isCached: true
  };
}

注意点と落とし穴

注意点①:プロパティ名の重複

複数の型で同じプロパティ名を使用する場合、型が矛盾するとエラーになります。

type TypeA = {
  id: string;
  value: number;
};

type TypeB = {
  id: string;  // OK: 同じ型
  value: string; // エラー: 型が異なる
};

// これはコンパイルエラーになる
type Combined = TypeA & TypeB; // TypeB['value'] は never になる

このような場合は、プロパティ名を明確に区別するか、共通部分を別の型として定義し直すべきです。

注意点②:過度な複雑性

複数の型を組み合わせすぎると、型チェックが遅くなったり、エラーメッセージが複雑になったりします。実務では「5個以上の型を組み合わせる場合は、新しい基本型を定義し直す」というルールを持つと良いでしょう。

// 避けるべき:複雑すぎる
type TooComplex = TypeA & TypeB & TypeC & TypeD & TypeE & TypeF & TypeG;

// 推奨:論理的にグループ化する
type BaseInfo = TypeA & TypeB;
type SecurityInfo = TypeC & TypeD;
type AuditInfo = TypeE & TypeF & TypeG;
type Complete = BaseInfo & SecurityInfo & AuditInfo;

注意点③:Intersection Typeと Optional の相互作用

Optional プロパティとIntersection Typeを組み合わせる場合、予期しない型になることがあります。

type TypeA = {
  id: string;
  optional?: string;
};

type TypeB = {
  optional: number;  // 必須
};

type Combined = TypeA & TypeB;

const obj: Combined = {
  id: 'test',
  optional: 42  // string | number ではなく、number である必要がある
};

注意点④:実行時の型チェックが存在しない

TypeScriptの型はコンパイル時にのみ有効です。実行時には型情報が削除されます。そのため、外部データ(API レスポンスなど)に対しては、実行時のバリデーションが必須です。

// 型安全だが、実行時には検証されない
type ExpectedData = {
  id: string;
  count: number;
};

async function fetchData(): Promise {
  const response = await fetch('/api/data');
  const data = await response.json();
  
  // ここで型がマッチするとしても、実行時の検証がない
  return data as ExpectedData;  // 危険
}

// 推奨:実行時バリデーション
function isValidData(data: any): data is ExpectedData {
  return (
    typeof data === 'object' &&
    data !== null &&
    typeof data.id === 'string' &&
    typeof data.count === 'number'
  );
}

async function fetchDataSafely(): Promise {
  const response = await fetch('/api/data');
  const data = await response.json();
  
  if (!isValidData(data)) {
    throw new Error('Invalid data format');
  }
  
  return data;
}

まとめ

TypeScriptのIntersection Type は、複数の型を合成して、より精密で複雑なデータ構造を型安全に扱うための強力なツールです。実務では以下のシーンで活躍します:

  • 複数のAPIレスポンスの統合
  • ミドルウェアによるリクエストの段階的な拡張
  • 権限管理と業務ロジックの結合
  • 複数の検証結果の統合
  • プラグインアーキテクチャの実装

使う際のポイントは:

  • 論理的にグループ化して、過度な複雑性を避ける
  • プロパティ名の重複に注意する
  • 外部データに対しては実行時バリデーションを必ず行う
  • 型ガード関数やジェネリクスと組み合わせると、さらに強力になる

Intersection Typeを正しく使いこなすことで、TypeScript による型安全なプログラミングを次のレベルへ進めることができます。大規模なプロジェクトでは特に、細かい型設計が後々の保守性に大きな影響を与えるため、今回紹介した実装パターンを参考に、プロジェクトに適した型設計を心がけてください。

タイトルとURLをコピーしました