DT Frontend TypeScript 実践パターン集

TypeScript 実践パターン集 - 型安全性を最大化する高度なテクニック

TypeScriptの高度な型システムを活用した実践的なパターンを詳しく解説。Utility Types、Template Literal Types、Conditional Typesなど、実際のプロジェクトで役立つテクニックを豊富なサンプルコードとともに紹介。

約5分で読めます
技術記事
実践的

この記事のポイント

TypeScriptの高度な型システムを活用した実践的なパターンを詳しく解説。Utility Types、Template Literal Types、Conditional Typesなど、実際のプロジェクトで役立つテクニックを豊富なサンプルコードとともに紹介。

この記事では、実践的なアプローチで技術的な課題を解決する方法を詳しく解説します。具体的なコード例とともに、ベストプラクティスを学ぶことができます。

TypeScriptの真の力は、その高度な型システムにあります。本記事では、実際のプロジェクトで活用できる実践的なTypeScriptパターンを、具体的なユースケースとともに詳しく解説します。初心者から上級者まで、段階的に学べる構成でお届けします。

🎯 型安全なAPI設計パターン

REST APIクライアントの型定義

// 基本的なAPIレスポンス型
interface ApiResponse<T> {
  data: T;
  status: 'success' | 'error';
  message?: string;
  timestamp: string;
}

// エンドポイント定義の型安全なパターン
type HTTPMethod = 'GET' | 'POST' | 'PUT' | 'DELETE' | 'PATCH';

interface EndpointConfig<TRequest = void, TResponse = unknown> {
  method: HTTPMethod;
  path: string;
  requestType: TRequest;
  responseType: TResponse;
}

// 具体的なAPI定義
type UserAPI = {
  getUsers: EndpointConfig<void, ApiResponse<User[]>>;
  getUser: EndpointConfig<{ id: number }, ApiResponse<User>>;
  createUser: EndpointConfig<CreateUserRequest, ApiResponse<User>>;
  updateUser: EndpointConfig<UpdateUserRequest, ApiResponse<User>>;
  deleteUser: EndpointConfig<{ id: number }, ApiResponse<void>>;
};

interface User {
  id: number;
  name: string;
  email: string;
  role: 'admin' | 'user' | 'moderator';
  createdAt: string;
  updatedAt: string;
}

interface CreateUserRequest {
  name: string;
  email: string;
  role?: User['role'];
}

interface UpdateUserRequest extends Partial<CreateUserRequest> {
  id: number;
}

型安全なAPIクライアント実装

// APIクライアントの基盤クラス
class TypedApiClient<TEndpoints> {
  constructor(private baseUrl: string) {}

  async request<K extends keyof TEndpoints>(
    endpoint: K,
    ...args: TEndpoints[K] extends EndpointConfig<infer TReq, any>
      ? TReq extends void
        ? []
        : [TReq]
      : never
  ): Promise<TEndpoints[K] extends EndpointConfig<any, infer TRes> ? TRes : never> {
    // 実装の詳細...
    return {} as any;
  }
}

// 使用例
const userApi = new TypedApiClient<UserAPI>('https://api.example.com');

// 型安全な呼び出し
const users = await userApi.request('getUsers'); // パラメータ不要
const user = await userApi.request('getUser', { id: 1 }); // id必須
const newUser = await userApi.request('createUser', {
  name: 'Alice',
  email: 'alice@example.com'
});

// TypeScriptが型エラーを検出
// const invalid = await userApi.request('getUser'); // ❌ パラメータが不足
// const invalid2 = await userApi.request('createUser', { name: 'Alice' }); // ❌ emailが不足

🔧 Utility Typesの実践活用

条件付きプロパティの実装

// 条件に基づいてプロパティを動的に変更
type ConditionalProps<T, K extends keyof T> = T[K] extends boolean
  ? T[K] extends true
    ? Required<T>
    : Partial<T>
  : T;

interface FormConfig {
  showAdvanced: boolean;
  validateOnBlur: boolean;
  autoSave: boolean;
}

interface FormProps<T extends FormConfig> {
  config: T;
  data: ConditionalProps<
    {
      username: string;
      email: string;
      password: string;
      confirmPassword: string;
      newsletter: boolean;
      termsAccepted: boolean;
    },
    'showAdvanced'
  >;
}

// 使用例
const basicForm: FormProps<{ showAdvanced: false; validateOnBlur: true; autoSave: false }> = {
  config: { showAdvanced: false, validateOnBlur: true, autoSave: false },
  data: {
    username: 'john_doe',
    email: 'john@example.com',
    // password, confirmPassword は省略可能
  }
};

const advancedForm: FormProps<{ showAdvanced: true; validateOnBlur: true; autoSave: true }> = {
  config: { showAdvanced: true, validateOnBlur: true, autoSave: true },
  data: {
    username: 'john_doe',
    email: 'john@example.com',
    password: 'secret123',
    confirmPassword: 'secret123',
    newsletter: true,
    termsAccepted: true, // すべてのプロパティが必須
  }
};

深いオブジェクトの型操作

// ネストしたオブジェクトのパス型を生成
type PathsToStringProps<T> = T extends string
  ? []
  : {
      [K in Extract<keyof T, string>]: [K, ...PathsToStringProps<T[K]>];
    }[Extract<keyof T, string>];

type JoinPaths<T extends ReadonlyArray<string | number>> = T extends readonly [
  infer Head,
  ...infer Tail
]
  ? Head extends string
    ? Tail extends ReadonlyArray<string | number>
      ? Tail['length'] extends 0
        ? Head
        : `${Head}.${JoinPaths<Tail>}`
      : never
    : never
  : never;

// 実践例:設定オブジェクトのパス型生成
interface AppConfig {
  database: {
    host: string;
    port: number;
    credentials: {
      username: string;
      password: string;
    };
  };
  api: {
    baseUrl: string;
    timeout: number;
    retries: number;
  };
  features: {
    enableCache: boolean;
    enableLogging: boolean;
  };
}

type ConfigPaths = JoinPaths<PathsToStringProps<AppConfig>>;
// "database.host" | "database.port" | "database.credentials.username" | 
// "database.credentials.password" | "api.baseUrl" | "api.timeout" | ...

// 型安全な設定値取得関数
function getConfigValue<T extends ConfigPaths>(
  config: AppConfig,
  path: T
): T extends 'database.host' ? string
  : T extends 'database.port' ? number
  : T extends 'features.enableCache' ? boolean
  : unknown {
  const keys = path.split('.');
  let value: any = config;
  
  for (const key of keys) {
    value = value[key];
  }
  
  return value;
}

// 使用例
const config: AppConfig = {
  database: {
    host: 'localhost',
    port: 5432,
    credentials: { username: 'admin', password: 'secret' }
  },
  api: { baseUrl: 'https://api.example.com', timeout: 5000, retries: 3 },
  features: { enableCache: true, enableLogging: false }
};

const host = getConfigValue(config, 'database.host'); // string型
const port = getConfigValue(config, 'database.port'); // number型
const cacheEnabled = getConfigValue(config, 'features.enableCache'); // boolean型

🎨 Template Literal Typesの活用

動的なイベント型の生成

// イベント名とペイロードの型安全な管理
type EventMap = {
  'user.created': { userId: number; email: string };
  'user.updated': { userId: number; changes: Partial<User> };
  'user.deleted': { userId: number };
  'order.placed': { orderId: string; userId: number; total: number };
  'order.shipped': { orderId: string; trackingNumber: string };
  'order.delivered': { orderId: string; deliveredAt: string };
};

// Template Literal Typesでより柔軟なイベント型を生成
type EntityEvents<T extends string> = 
  | `${T}.created`
  | `${T}.updated` 
  | `${T}.deleted`
  | `${T}.fetched`;

type UserEvents = EntityEvents<'user'>; // 'user.created' | 'user.updated' | 'user.deleted' | 'user.fetched'
type ProductEvents = EntityEvents<'product'>; // 'product.created' | 'product.updated' | ...

// 動的なAPI URL生成
type ApiVersion = 'v1' | 'v2' | 'v3';
type HttpMethod = 'GET' | 'POST' | 'PUT' | 'DELETE';
type ResourcePath<T extends string> = `/${T}` | `/${T}/:id`;

type ApiUrl<V extends ApiVersion, R extends string> = `/api/${V}${ResourcePath<R>}`;

// 使用例
type UserApiUrls = ApiUrl<'v1', 'users'>; // '/api/v1/users' | '/api/v1/users/:id'
type ProductApiUrls = ApiUrl<'v2', 'products'>; // '/api/v2/products' | '/api/v2/products/:id'

// 型安全なルーティング
function createApiRoute<V extends ApiVersion, R extends string>(
  version: V,
  resource: R,
  id?: string
): ApiUrl<V, R> {
  const basePath = `/api/${version}/${resource}` as const;
  return (id ? `${basePath}/:id` : basePath) as ApiUrl<V, R>;
}

CSS-in-JSの型安全な実装

// CSS プロパティの型定義
type CSSUnit = 'px' | 'em' | 'rem' | '%' | 'vh' | 'vw' | 'vmin' | 'vmax';
type CSSValue<T extends string> = `${number}${T}` | '0';
type Size = CSSValue<CSSUnit> | 'auto' | 'inherit' | 'initial';

// Template Literal Typesで柔軟なCSS型を生成
type ColorValue = 
  | `#${string}` 
  | `rgb(${number}, ${number}, ${number})` 
  | `rgba(${number}, ${number}, ${number}, ${number})`
  | `hsl(${number}, ${number}%, ${number}%)`
  | 'transparent' | 'currentColor';

interface StyleProps {
  width?: Size;
  height?: Size;
  margin?: Size;
  padding?: Size;
  backgroundColor?: ColorValue;
  color?: ColorValue;
  fontSize?: CSSValue<'px' | 'em' | 'rem'>;
  borderRadius?: CSSValue<'px' | 'em' | 'rem'>;
}

// 型安全なスタイル関数
function createStyles<T extends Record<string, StyleProps>>(styles: T): T {
  return styles;
}

// 使用例
const buttonStyles = createStyles({
  primary: {
    backgroundColor: '#007bff',
    color: '#ffffff',
    padding: '12px',
    borderRadius: '4px',
    fontSize: '16px'
  },
  secondary: {
    backgroundColor: 'transparent',
    color: '#007bff',
    padding: '12px',
    borderRadius: '4px',
    fontSize: '16px'
  }
});

// TypeScriptが不正な値を検出
// const invalidStyles = createStyles({
//   error: {
//     backgroundColor: 'invalid-color', // ❌ 型エラー
//     padding: '12', // ❌ 単位が不足
//   }
// });

🔍 Conditional Typesの実践パターン

関数オーバーロードの代替

// 従来のオーバーロード
function processData(input: string): string;
function processData(input: number): number;
function processData(input: boolean): boolean;
function processData(input: string | number | boolean): string | number | boolean {
  return input;
}

// Conditional Typesを使った型安全な実装
type ProcessResult<T> = T extends string
  ? string
  : T extends number
  ? number
  : T extends boolean
  ? boolean
  : never;

function processDataTyped<T extends string | number | boolean>(input: T): ProcessResult<T> {
  return input as ProcessResult<T>;
}

// より複雑な例:データ変換関数
interface TransformMap {
  'json': object;
  'text': string;
  'number': number;
  'boolean': boolean;
  'date': Date;
}

function transformData<K extends keyof TransformMap>(
  data: string,
  type: K
): TransformMap[K] {
  switch (type) {
    case 'json':
      return JSON.parse(data) as TransformMap[K];
    case 'text':
      return data as TransformMap[K];
    case 'number':
      return Number(data) as TransformMap[K];
    case 'boolean':
      return Boolean(data) as TransformMap[K];
    case 'date':
      return new Date(data) as TransformMap[K];
    default:
      throw new Error(`Unsupported type: ${type}`);
  }
}

// 使用例 - 戻り値の型が自動で決まる
const jsonData = transformData('{"name": "Alice"}', 'json'); // object型
const textData = transformData('Hello', 'text'); // string型
const numberData = transformData('42', 'number'); // number型
const dateData = transformData('2024-01-01', 'date'); // Date型

状態管理の型安全性

// Reduxライクな状態管理の型定義
interface BaseAction {
  type: string;
  payload?: any;
}

type ActionMap<M extends Record<string, any>> = {
  [Key in keyof M]: M[Key] extends undefined
    ? { type: Key }
    : { type: Key; payload: M[Key] };
};

// アクション型の定義
type UserActionPayloads = {
  'FETCH_USERS_REQUEST': undefined;
  'FETCH_USERS_SUCCESS': { users: User[] };
  'FETCH_USERS_FAILURE': { error: string };
  'CREATE_USER_REQUEST': { userData: CreateUserRequest };
  'CREATE_USER_SUCCESS': { user: User };
  'CREATE_USER_FAILURE': { error: string };
};

type UserActions = ActionMap<UserActionPayloads>[keyof ActionMap<UserActionPayloads>];

// 状態の型定義
interface UserState {
  users: User[];
  loading: boolean;
  error: string | null;
  currentUser: User | null;
}

// 型安全なReducer
function userReducer(state: UserState, action: UserActions): UserState {
  switch (action.type) {
    case 'FETCH_USERS_REQUEST':
      return { ...state, loading: true, error: null };
    
    case 'FETCH_USERS_SUCCESS':
      return { 
        ...state, 
        loading: false, 
        users: action.payload.users // payload.usersが型安全
      };
    
    case 'FETCH_USERS_FAILURE':
      return { 
        ...state, 
        loading: false, 
        error: action.payload.error // payload.errorが型安全
      };
    
    case 'CREATE_USER_SUCCESS':
      return {
        ...state,
        users: [...state.users, action.payload.user] // payload.userが型安全
      };
    
    default:
      return state;
  }
}

// アクションクリエーターの型定義
type ActionCreators<T extends Record<string, any>> = {
  [K in keyof T]: T[K] extends undefined
    ? () => { type: K }
    : (payload: T[K]) => { type: K; payload: T[K] };
};

// 型安全なアクションクリエーター
const userActions: ActionCreators<UserActionPayloads> = {
  'FETCH_USERS_REQUEST': () => ({ type: 'FETCH_USERS_REQUEST' }),
  'FETCH_USERS_SUCCESS': (payload) => ({ type: 'FETCH_USERS_SUCCESS', payload }),
  'FETCH_USERS_FAILURE': (payload) => ({ type: 'FETCH_USERS_FAILURE', payload }),
  'CREATE_USER_REQUEST': (payload) => ({ type: 'CREATE_USER_REQUEST', payload }),
  'CREATE_USER_SUCCESS': (payload) => ({ type: 'CREATE_USER_SUCCESS', payload }),
  'CREATE_USER_FAILURE': (payload) => ({ type: 'CREATE_USER_FAILURE', payload }),
};

🚀 実践での活用ポイント

型定義の段階的な強化

// 1. 基本的な型定義から開始
interface BasicUser {
  id: number;
  name: string;
  email: string;
}

// 2. ビジネスロジックに応じて拡張
interface EnhancedUser extends BasicUser {
  role: 'admin' | 'user' | 'moderator';
  permissions: string[];
  lastLoginAt: Date | null;
  isActive: boolean;
}

// 3. 実行時検証との組み合わせ
import { z } from 'zod';

const UserSchema = z.object({
  id: z.number().positive(),
  name: z.string().min(1).max(100),
  email: z.string().email(),
  role: z.enum(['admin', 'user', 'moderator']),
  permissions: z.array(z.string()),
  lastLoginAt: z.date().nullable(),
  isActive: z.boolean()
});

// TypeScriptの型とランタイム検証の統合
type ValidatedUser = z.infer<typeof UserSchema>;

function validateUser(data: unknown): ValidatedUser {
  return UserSchema.parse(data);
}

パフォーマンス最適化

// 大きな型の遅延評価
type LazyAPITypes<T> = T extends (...args: any[]) => any
  ? ReturnType<T>
  : T extends Promise<infer U>
  ? U
  : T;

// 型計算の最適化
type OptimizedPick<T, K extends keyof T> = Pick<T, K>;
type OptimizedOmit<T, K extends keyof T> = Omit<T, K>;

// インデックス型の効率的な使用
interface UserLookup {
  [id: number]: User;
}

// Map型を活用した型安全なキャッシュ
class TypedCache<T> {
  private cache = new Map<string, T>();
  
  set(key: string, value: T): void {
    this.cache.set(key, value);
  }
  
  get(key: string): T | undefined {
    return this.cache.get(key);
  }
  
  has(key: string): boolean {
    return this.cache.has(key);
  }
}

const userCache = new TypedCache<User>();

📚 まとめ

TypeScriptの高度な型システムを活用することで、以下のメリットが得られます:

主要な利点

  • 実行時エラーの削減: コンパイル時に多くのエラーを検出
  • リファクタリングの安全性: 型システムが変更の影響範囲を明確化
  • 開発効率の向上: IDEのサポートによる正確なオートコンプリート
  • コードの自己文書化: 型定義がドキュメントとして機能

推奨される導入順序

  1. 基本的な型アノテーションから開始
  2. Utility Typesで既存の型を活用
  3. Conditional Typesで動的な型生成
  4. Template Literal Typesで文字列ベースの型安全性

これらのパターンを段階的に導入することで、TypeScriptの真の力を活用した、堅牢で保守性の高いアプリケーションを構築できます。公式ドキュメントと併せて学習を進め、実際のプロジェクトで積極的に活用していきましょう。