← 🏠 記事一覧に戻る
📅📁backend🏷️

📝 TypeScript高度な型活用テクニック

Conditional Types、Mapped Types、Template Literal Typesなど、TypeScriptの高度な型システムを実践的に解説

TypeScript高度な型活用テクニック

TypeScriptの型システムは非常に強力で、適切に活用することで実行時エラーを大幅に削減できます。今回は実際の開発で役立つ高度な型のテクニックを紹介します。

Conditional Typesで条件付き型定義

Conditional Typesを使用すると、型の条件に基づいて異なる型を返すことができます。

基本的な使い方

// T が string を継承する場合は number、そうでなければ boolean を返す
type ConditionalType<T> = T extends string ? number : boolean;

type Result1 = ConditionalType<string>;    // number
type Result2 = ConditionalType<number>;    // boolean
type Result3 = ConditionalType<'hello'>;   // number (string literal は string を継承)

実践的な例:API レスポンス型の自動生成

interface User {
  id: number;
  name: string;
  email: string;
}

interface Post {
  id: number;
  title: string;
  content: string;
  authorId: number;
}

// APIのレスポンス型を自動生成
type ApiResponse<T> = T extends 'user' 
  ? { data: User; status: 'success' }
  : T extends 'post'
  ? { data: Post; status: 'success' }
  : { error: string; status: 'error' };

// 型安全なAPIクライアント
async function fetchData<T extends 'user' | 'post'>(
  endpoint: T,
  id: number
): Promise<ApiResponse<T>> {
  const response = await fetch(`/api/${endpoint}/${id}`);
  
  if (!response.ok) {
    return { error: 'Failed to fetch', status: 'error' } as ApiResponse<T>;
  }
  
  const data = await response.json();
  return { data, status: 'success' } as ApiResponse<T>;
}

// 使用例:型推論が効く
const userResponse = await fetchData('user', 1);
// userResponse.data は User 型として推論される

const postResponse = await fetchData('post', 1);
// postResponse.data は Post 型として推論される

Mapped Typesで既存型の変換

Mapped Typesを使用すると、既存の型のプロパティを変換して新しい型を作成できます。

基本的なMapped Types

interface User {
  id: number;
  name: string;
  email: string;
  age: number;
}

// すべてのプロパティをオプショナルにする
type PartialUser = {
  [K in keyof User]?: User[K];
};

// すべてのプロパティを読み取り専用にする
type ReadonlyUser = {
  readonly [K in keyof User]: User[K];
};

// 特定のプロパティのみを選択
type UserSummary = {
  [K in 'id' | 'name']: User[K];
};

実践的な例:フォームバリデーション

// バリデーションルール型
type ValidationRule<T> = {
  required?: boolean;
  minLength?: T extends string ? number : never;
  maxLength?: T extends string ? number : never;
  min?: T extends number ? number : never;
  max?: T extends number ? number : never;
  pattern?: T extends string ? RegExp : never;
};

// フォーム型からバリデーション設定型を自動生成
type FormValidation<T> = {
  [K in keyof T]?: ValidationRule<T[K]>;
};

interface RegistrationForm {
  username: string;
  email: string;
  age: number;
  password: string;
}

// 型安全なバリデーション設定
const validationRules: FormValidation<RegistrationForm> = {
  username: {
    required: true,
    minLength: 3,
    maxLength: 20,
  },
  email: {
    required: true,
    pattern: /^[^\s@]+@[^\s@]+\.[^\s@]+$/,
  },
  age: {
    required: true,
    min: 18,
    max: 120,
  },
  password: {
    required: true,
    minLength: 8,
  },
};

// バリデーション関数
function validateForm<T>(
  data: T,
  rules: FormValidation<T>
): { isValid: boolean; errors: Partial<Record<keyof T, string>> } {
  const errors: Partial<Record<keyof T, string>> = {};

  for (const key in rules) {
    const value = data[key];
    const rule = rules[key];

    if (!rule) continue;

    if (rule.required && !value) {
      errors[key] = `${String(key)} is required`;
      continue;
    }

    if (typeof value === 'string') {
      if (rule.minLength && value.length < rule.minLength) {
        errors[key] = `${String(key)} must be at least ${rule.minLength} characters`;
      }
      if (rule.maxLength && value.length > rule.maxLength) {
        errors[key] = `${String(key)} must be at most ${rule.maxLength} characters`;
      }
      if (rule.pattern && !rule.pattern.test(value)) {
        errors[key] = `${String(key)} format is invalid`;
      }
    }

    if (typeof value === 'number') {
      if (rule.min && value < rule.min) {
        errors[key] = `${String(key)} must be at least ${rule.min}`;
      }
      if (rule.max && value > rule.max) {
        errors[key] = `${String(key)} must be at most ${rule.max}`;
      }
    }
  }

  return {
    isValid: Object.keys(errors).length === 0,
    errors,
  };
}

Template Literal Typesで動的な型生成

Template Literal Typesを使用すると、文字列リテラル型を組み合わせて新しい型を作成できます。

CSS-in-JSの型安全性

// カラーパレット
type Color = 'red' | 'blue' | 'green' | 'yellow';
type Shade = '100' | '200' | '300' | '400' | '500';

// 動的にカラークラス名を生成
type ColorClass = `text-${Color}-${Shade}` | `bg-${Color}-${Shade}`;

// 使用例
const validClasses: ColorClass[] = [
  'text-red-100',
  'bg-blue-500',
  'text-green-300',
  // 'text-purple-100', // エラー: purple は Color に含まれない
];

// より複雑な例:レスポンシブクラス
type Breakpoint = 'sm' | 'md' | 'lg' | 'xl';
type ResponsiveColorClass = ColorClass | `${Breakpoint}:${ColorClass}`;

const responsiveClasses: ResponsiveColorClass[] = [
  'text-red-100',
  'md:bg-blue-500',
  'lg:text-green-300',
];

APIエンドポイントの型安全性

// RESTful APIエンドポイント型
type HttpMethod = 'GET' | 'POST' | 'PUT' | 'DELETE';
type Resource = 'users' | 'posts' | 'comments';

type ApiEndpoint<M extends HttpMethod, R extends Resource> = 
  M extends 'GET'
    ? `/api/${R}` | `/api/${R}/${number}`
    : M extends 'POST'
    ? `/api/${R}`
    : M extends 'PUT' | 'DELETE'
    ? `/api/${R}/${number}`
    : never;

// 型安全なAPIクライアント
async function apiRequest<M extends HttpMethod, R extends Resource>(
  method: M,
  endpoint: ApiEndpoint<M, R>,
  data?: any
) {
  const response = await fetch(endpoint, {
    method,
    headers: {
      'Content-Type': 'application/json',
    },
    body: data ? JSON.stringify(data) : undefined,
  });

  return response.json();
}

// 使用例
apiRequest('GET', '/api/users');           // ✅ 有効
apiRequest('GET', '/api/users/123');       // ✅ 有効
apiRequest('POST', '/api/posts');          // ✅ 有効
apiRequest('PUT', '/api/comments/456');    // ✅ 有効
// apiRequest('POST', '/api/users/123');   // ❌ エラー: POSTは個別リソースに対して無効

Utility Typesの組み合わせ技

TypeScriptの組み込みUtility Typesを組み合わせることで、複雑な型変換を簡潔に表現できます。

深い部分オプショナル化

// ネストした型の部分的オプショナル化
type DeepPartial<T> = {
  [P in keyof T]?: T[P] extends object ? DeepPartial<T[P]> : T[P];
};

interface Config {
  database: {
    host: string;
    port: number;
    credentials: {
      username: string;
      password: string;
    };
  };
  api: {
    baseUrl: string;
    timeout: number;
  };
}

// 部分的な設定の上書きが可能
function updateConfig(updates: DeepPartial<Config>): Config {
  // 既存の設定とマージするロジック
  return mergeDeep(defaultConfig, updates);
}

// 使用例
updateConfig({
  database: {
    port: 5432, // host や credentials は省略可能
  },
});

型安全なイベントシステム

// イベント型定義
interface EventMap {
  'user:login': { userId: string; timestamp: Date };
  'user:logout': { userId: string };
  'post:created': { postId: string; authorId: string; title: string };
  'post:updated': { postId: string; changes: string[] };
}

type EventName = keyof EventMap;

// 型安全なイベントエミッター
class TypedEventEmitter {
  private listeners: {
    [K in EventName]?: Array<(data: EventMap[K]) => void>;
  } = {};

  on<K extends EventName>(
    event: K,
    listener: (data: EventMap[K]) => void
  ): void {
    if (!this.listeners[event]) {
      this.listeners[event] = [];
    }
    this.listeners[event]!.push(listener);
  }

  emit<K extends EventName>(event: K, data: EventMap[K]): void {
    const eventListeners = this.listeners[event];
    if (eventListeners) {
      eventListeners.forEach(listener => listener(data));
    }
  }

  off<K extends EventName>(
    event: K,
    listener: (data: EventMap[K]) => void
  ): void {
    const eventListeners = this.listeners[event];
    if (eventListeners) {
      this.listeners[event] = eventListeners.filter(l => l !== listener);
    }
  }
}

// 使用例
const emitter = new TypedEventEmitter();

emitter.on('user:login', (data) => {
  // data は { userId: string; timestamp: Date } として型推論される
  console.log(`User ${data.userId} logged in at ${data.timestamp}`);
});

emitter.emit('user:login', {
  userId: '123',
  timestamp: new Date(),
});

// emitter.emit('user:login', { userId: 123 }); // エラー: userId は string である必要がある

まとめ

TypeScriptの高度な型システムを活用することで:

  1. 実行時エラーの削減: コンパイル時により多くのエラーを検出
  2. 開発効率の向上: IDEの補完機能がより正確に
  3. 保守性の向上: 型が仕様書の役割を果たす
  4. リファクタリングの安全性: 型システムが変更の影響範囲を明確化

これらのテクニックを組み合わせることで、より堅牢で保守性の高いTypeScriptアプリケーションを構築できます。型システムの力を最大限に活用して、品質の高いコードを書いていきましょう。