T-CREATOR

TypeScript 型システムを使った実行時バリデーション自動生成テクニック

TypeScript 型システムを使った実行時バリデーション自動生成テクニック

TypeScript で開発していて「API から受け取ったデータの型チェックが面倒」「バリデーションロジックと型定義を二重管理している」といった悩みを抱えていませんか?コンパイル時の型安全性は確保できても、実行時のデータ検証は別途実装が必要で、保守性や一貫性の課題に直面することが多いでしょう。

実は、TypeScript の高度な型システムを活用することで、型定義から実行時バリデーションを自動生成し、開発効率と品質を劇的に向上させることができます。この記事では、Template Literal Types、Mapped Types、Conditional Types などの先進的な型機能を駆使した、型駆動開発(Type-Driven Development)の実践手法を詳しく解説します。

背景:従来のバリデーション実装の課題と型システムの可能性

現代の Web 開発において、データの検証は避けて通れない重要な要素です。しかし、従来のアプローチでは多くの課題を抱えており、TypeScript の型システムが持つ潜在能力を十分に活用できていないのが現状です。

従来のバリデーション実装における根本的な問題

実際の開発現場では、以下のような問題が頻繁に発生しています。

型定義とバリデーションロジックの分離による保守性の悪化

typescript// 従来のアプローチ:型定義とバリデーションが分離
interface User {
  id: number;
  name: string;
  email: string;
  age: number;
  isActive: boolean;
}

// 別途バリデーション関数を手動実装
function validateUser(data: unknown): data is User {
  if (typeof data !== 'object' || data === null) {
    return false;
  }

  const obj = data as Record<string, unknown>;

  return (
    typeof obj.id === 'number' &&
    typeof obj.name === 'string' &&
    typeof obj.email === 'string' &&
    typeof obj.age === 'number' &&
    typeof obj.isActive === 'boolean'
  );
}

この手法では、User インターフェースを変更するたびに、validateUser 関数も手動で更新する必要があり、同期が取れなくなるリスクが常に存在します。

複雑な型構造での実装コストの増大

typescript// 複雑な型構造の例
interface ApiResponse<T> {
  data: T;
  meta: {
    total: number;
    page: number;
    limit: number;
  };
  errors?: Array<{
    field: string;
    message: string;
    code: string;
  }>;
}

interface UserListResponse extends ApiResponse<User[]> {
  filters: {
    department?: string;
    role?: 'admin' | 'user' | 'guest';
    status?: 'active' | 'inactive';
  };
}

このような複雑な型構造に対して手動でバリデーション関数を実装するのは、非常に時間がかかり、エラーが発生しやすくなります。

TypeScript 型システムが提供する新たな可能性

TypeScript 4.0 以降で導入された高度な型機能により、従来は不可能だった型レベルでの動的な処理が実現可能になりました。

Template Literal Types による動的型生成

typescript// Template Literal Types を活用した動的な型生成例
type EventName = 'click' | 'hover' | 'focus';
type ElementType = 'button' | 'input' | 'div';

// 動的にイベントハンドラー名を生成
type EventHandler<
  E extends EventName,
  T extends ElementType
> = `on${Capitalize<E>}${Capitalize<T>}`;

// 結果: 'onClickButton' | 'onHoverInput' | 'onFocusDiv' など
type ButtonClickHandler = EventHandler<'click', 'button'>;

Mapped Types による型変換の自動化

typescript// Mapped Types を使った型変換の例
type ValidationResult<T> = {
  [K in keyof T]: {
    value: T[K];
    isValid: boolean;
    errors: string[];
  };
};

// User型から自動的にバリデーション結果型を生成
type UserValidationResult = ValidationResult<User>;
// 結果:
// {
//   id: { value: number; isValid: boolean; errors: string[] };
//   name: { value: string; isValid: boolean; errors: string[] };
//   email: { value: string; isValid: boolean; errors: string[] };
//   age: { value: number; isValid: boolean; errors: string[] };
//   isActive: { value: boolean; isValid: boolean; errors: string[] };
// }

これらの機能を組み合わせることで、型定義から実行時バリデーションを自動生成する革新的なアプローチが可能になります。

課題:手動バリデーション実装による保守性・一貫性の問題

実際の開発プロジェクトにおいて、手動でのバリデーション実装がもたらす具体的な課題を詳しく分析してみましょう。

課題 1:型定義とバリデーションロジックの同期問題

実際のプロジェクトで発生する同期ずれの例

typescript// 初期の型定義
interface Product {
  id: number;
  name: string;
  price: number;
  category: string;
}

// 対応するバリデーション関数
function validateProduct(data: unknown): data is Product {
  if (typeof data !== 'object' || data === null)
    return false;
  const obj = data as Record<string, unknown>;

  return (
    typeof obj.id === 'number' &&
    typeof obj.name === 'string' &&
    typeof obj.price === 'number' &&
    typeof obj.category === 'string'
  );
}

// 後日、型定義が変更される
interface Product {
  id: number;
  name: string;
  price: number;
  category: string;
  description?: string; // 新しいフィールド追加
  tags: string[]; // 新しいフィールド追加
  isAvailable: boolean; // 新しいフィールド追加
}

// ❌ バリデーション関数の更新を忘れがち
// validateProduct関数は古いままで、新しいフィールドをチェックしない

この同期ずれにより、実行時に予期しないエラーが発生したり、セキュリティホールが生まれる可能性があります。

課題 2:複雑な型構造での実装負荷

ネストした型構造のバリデーション実装

typescript// 複雑にネストした型構造
interface OrderDetails {
  order: {
    id: string;
    items: Array<{
      productId: number;
      quantity: number;
      options?: {
        size?: 'S' | 'M' | 'L';
        color?: string;
        customization?: Record<string, string | number>;
      };
    }>;
    shipping: {
      address: {
        street: string;
        city: string;
        zipCode: string;
        country: string;
      };
      method: 'standard' | 'express' | 'overnight';
      cost: number;
    };
    payment: {
      method: 'credit' | 'debit' | 'paypal';
      amount: number;
      currency: string;
      installments?: number;
    };
  };
  customer: {
    id: string;
    profile: {
      name: string;
      email: string;
      phone?: string;
    };
    preferences: {
      notifications: boolean;
      marketing: boolean;
    };
  };
}

// 手動でのバリデーション実装は非常に複雑になる
function validateOrderDetails(
  data: unknown
): data is OrderDetails {
  // 100行以上のバリデーションコードが必要...
  // エラーが発生しやすく、保守が困難
}

課題 3:バリデーションルールの一貫性確保の困難さ

チーム開発での一貫性の問題

typescript// 開発者Aが実装したバリデーション
function validateUserA(data: unknown): data is User {
  // 厳密なチェック
  if (typeof data !== 'object' || data === null) {
    throw new Error('Invalid data type');
  }
  // 詳細なエラーメッセージ
}

// 開発者Bが実装したバリデーション
function validateUserB(data: unknown): data is User {
  // 緩いチェック
  if (!data) return false;
  // 簡素なエラーハンドリング
}

このような実装の違いにより、アプリケーション全体でのバリデーション品質にばらつきが生じてしまいます。

課題 4:パフォーマンスとメンテナンス性のトレードオフ

効率的なバリデーションの実装困難性

typescript// パフォーマンスを重視した実装
function fastValidateUser(data: unknown): data is User {
  // 最小限のチェックのみ
  return !!(data && typeof data === 'object');
}

// 安全性を重視した実装
function safeValidateUser(data: unknown): data is User {
  // 詳細なチェックだが処理が重い
  try {
    JSON.parse(JSON.stringify(data));
    // 複雑な検証ロジック...
  } catch {
    return false;
  }
}

手動実装では、パフォーマンスと安全性のバランスを取ることが困難で、プロジェクトの要件に応じた最適化が難しくなります。

解決策:TypeScript 型システムを活用した自動生成アプローチ

これらの課題を根本的に解決するため、TypeScript の高度な型システムを活用した革新的なアプローチを提案します。型定義を単一の情報源(Single Source of Truth)として、実行時バリデーションを自動生成する手法です。

基本的なアプローチ:型情報からバリデーターを自動生成

型レベルでのバリデーター生成の基盤

typescript// 基本的な型情報抽出のためのユーティリティ型
type TypeInfo<T> = T extends string
  ? 'string'
  : T extends number
  ? 'number'
  : T extends boolean
  ? 'boolean'
  : T extends Array<infer U>
  ? ['array', TypeInfo<U>]
  : T extends object
  ? { [K in keyof T]: TypeInfo<T[K]> }
  : 'unknown';

// 使用例
type UserTypeInfo = TypeInfo<User>;
// 結果: {
//   id: 'number';
//   name: 'string';
//   email: 'string';
//   age: 'number';
//   isActive: 'boolean';
// }

実行時バリデーター生成の基本実装

typescript// 型情報に基づく動的バリデーター生成
class TypeValidator {
  static create<T>(typeInfo: TypeInfo<T>) {
    return (data: unknown): data is T => {
      return this.validateByTypeInfo(data, typeInfo);
    };
  }

  private static validateByTypeInfo(
    data: unknown,
    typeInfo: any
  ): boolean {
    if (typeof typeInfo === 'string') {
      return typeof data === typeInfo;
    }

    if (
      Array.isArray(typeInfo) &&
      typeInfo[0] === 'array'
    ) {
      if (!Array.isArray(data)) return false;
      return data.every((item) =>
        this.validateByTypeInfo(item, typeInfo[1])
      );
    }

    if (typeof typeInfo === 'object' && typeInfo !== null) {
      if (typeof data !== 'object' || data === null)
        return false;
      const obj = data as Record<string, unknown>;

      return Object.entries(typeInfo).every(
        ([key, expectedType]) =>
          this.validateByTypeInfo(obj[key], expectedType)
      );
    }

    return false;
  }
}

高度な型システム機能の活用

Template Literal Types による動的スキーマ生成

typescript// Template Literal Types を使った動的なバリデーションルール生成
type ValidationRule =
  | 'required'
  | 'optional'
  | 'email'
  | 'min-length'
  | 'max-length'
  | 'numeric'
  | 'positive';

type FieldValidation<
  T extends string,
  R extends ValidationRule
> = `${T}:${R}`;

// 使用例:動的にバリデーションルールを組み合わせ
type UserValidationRules =
  | FieldValidation<'name', 'required'>
  | FieldValidation<'email', 'required' | 'email'>
  | FieldValidation<'age', 'numeric' | 'positive'>
  | FieldValidation<'phone', 'optional'>;

Mapped Types による包括的なバリデーション型生成

typescript// Mapped Types を使った包括的なバリデーション型の生成
type ValidationSchema<T> = {
  [K in keyof T]-?: {
    type: TypeInfo<T[K]>;
    required: T[K] extends undefined ? false : true;
    rules: ValidationRule[];
    customValidator?: (value: T[K]) => boolean;
  };
};

// 自動生成されるバリデーションスキーマ
type UserValidationSchema = ValidationSchema<User>;

この基盤により、型定義の変更が自動的にバリデーションロジックに反映され、保守性と一貫性の問題が根本的に解決されます。

具体例:高度な型システム機能を活用した実装パターン

ここからは、TypeScript の最新機能を駆使した具体的な実装例を詳しく解説します。実際のプロジェクトで即座に活用できる実践的なコード例とともに、各手法の特徴と適用場面を明確にしていきます。

Template Literal Types を使ったスキーマ生成

Template Literal Types は、文字列リテラル型を動的に組み合わせることで、型レベルでの文字列操作を可能にします。この機能を活用して、バリデーションスキーマを自動生成する手法を見ていきましょう。

動的バリデーションルール生成の実装

typescript// バリデーションルールの基本型定義
type BaseRule = 'required' | 'optional';
type StringRule = 'email' | 'url' | 'uuid' | 'alphanumeric';
type NumberRule =
  | 'positive'
  | 'negative'
  | 'integer'
  | 'float';
type LengthRule =
  | `min-${number}`
  | `max-${number}`
  | `length-${number}`;

// Template Literal Types による動的ルール組み合わせ
type ValidationRuleSet<T> = T extends string
  ? BaseRule | StringRule | LengthRule
  : T extends number
  ? BaseRule | NumberRule
  : T extends boolean
  ? BaseRule
  : T extends Array<infer U>
  ? BaseRule | `array-${ValidationRuleSet<U>}`
  : BaseRule;

// 実際の使用例
interface ProductForm {
  name: string;
  price: number;
  description?: string;
  tags: string[];
  isAvailable: boolean;
}

// 自動生成されるバリデーションルール型
type ProductValidationRules = {
  name: ValidationRuleSet<string>; // 'required' | 'email' | 'min-3' など
  price: ValidationRuleSet<number>; // 'required' | 'positive' など
  description: ValidationRuleSet<string | undefined>;
  tags: ValidationRuleSet<string[]>; // 'required' | 'array-required' など
  isAvailable: ValidationRuleSet<boolean>;
};

実行時バリデーター生成の実装

typescript// Template Literal Types を活用したバリデーター生成クラス
class SchemaValidator {
  private static parseRule(rule: string): {
    type: string;
    value?: number;
  } {
    const [type, value] = rule.split('-');
    return {
      type,
      value: value ? parseInt(value, 10) : undefined,
    };
  }

  static createValidator<T>(
    schema: Record<keyof T, string[]>
  ): (data: unknown) => data is T {
    return (data: unknown): data is T => {
      if (typeof data !== 'object' || data === null) {
        return false;
      }

      const obj = data as Record<string, unknown>;

      return Object.entries(schema).every(
        ([field, rules]) => {
          const value = obj[field];

          return rules.every((rule) => {
            const { type, value: ruleValue } =
              this.parseRule(rule);

            switch (type) {
              case 'required':
                return (
                  value !== undefined && value !== null
                );

              case 'optional':
                return true; // オプショナルフィールドは常に有効

              case 'email':
                return (
                  typeof value === 'string' &&
                  /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(value)
                );

              case 'min':
                return (
                  typeof value === 'string' &&
                  value.length >= (ruleValue || 0)
                );

              case 'max':
                return (
                  typeof value === 'string' &&
                  value.length <= (ruleValue || Infinity)
                );

              case 'positive':
                return (
                  typeof value === 'number' && value > 0
                );

              case 'array':
                return Array.isArray(value);

              default:
                return true;
            }
          });
        }
      );
    };
  }
}

// 使用例
const productSchema = {
  name: ['required', 'min-3', 'max-100'],
  price: ['required', 'positive'],
  description: ['optional', 'max-500'],
  tags: ['required', 'array'],
  isAvailable: ['required'],
};

const validateProduct =
  SchemaValidator.createValidator<ProductForm>(
    productSchema
  );

// 実際のバリデーション実行
const productData = {
  name: 'Sample Product',
  price: 1000,
  description: 'This is a sample product',
  tags: ['electronics', 'gadget'],
  isAvailable: true,
};

if (validateProduct(productData)) {
  // productData は ProductForm 型として安全に使用可能
  console.log(
    `Product: ${productData.name}, Price: ${productData.price}`
  );
} else {
  console.log(
    'Validation errors:',
    validateProduct(productData).errors
  );
}

Mapped Types による動的バリデーター作成

Mapped Types を使用することで、既存の型定義から動的にバリデーション関連の型を生成できます。これにより、型の変更が自動的にバリデーションロジックに反映されます。

包括的なバリデーション型の自動生成

typescript// Mapped Types を使った包括的なバリデーション型生成
type ValidationConfig<T> = {
  [K in keyof T]: {
    type: T[K] extends string
      ? 'string'
      : T[K] extends number
      ? 'number'
      : T[K] extends boolean
      ? 'boolean'
      : T[K] extends Array<infer U>
      ? 'array'
      : 'object';
    required: T[K] extends undefined ? false : true;
    rules: string[];
    transform?: (value: unknown) => T[K];
    customValidator?: (value: T[K]) => boolean | string;
  };
};

// 実際の型に対する自動生成例
interface UserProfile {
  id: number;
  username: string;
  email: string;
  age?: number;
  preferences: {
    theme: 'light' | 'dark';
    notifications: boolean;
  };
  roles: string[];
}

// 自動生成されるバリデーション設定型
type UserProfileValidation = ValidationConfig<UserProfile>;

動的バリデーター実装クラス

typescript// Mapped Types を活用した高度なバリデーター実装
class DynamicValidator<T> {
  private config: ValidationConfig<T>;

  constructor(config: ValidationConfig<T>) {
    this.config = config;
  }

  validate(data: unknown): {
    isValid: boolean;
    data?: T;
    errors: Record<string, string[]>;
  } {
    const errors: Record<string, string[]> = {};

    if (typeof data !== 'object' || data === null) {
      return {
        isValid: false,
        errors: { _root: ['Data must be an object'] },
      };
    }

    const obj = data as Record<string, unknown>;
    const result: Partial<T> = {};

    // 各フィールドのバリデーション実行
    for (const [field, fieldConfig] of Object.entries(
      this.config
    )) {
      const fieldErrors: string[] = [];
      const value = obj[field];

      // 必須チェック
      if (
        fieldConfig.required &&
        (value === undefined || value === null)
      ) {
        fieldErrors.push(`${field} is required`);
        continue;
      }

      // オプショナルフィールドで値が未定義の場合はスキップ
      if (
        !fieldConfig.required &&
        (value === undefined || value === null)
      ) {
        continue;
      }

      // 型チェック
      if (!this.validateType(value, fieldConfig.type)) {
        fieldErrors.push(
          `${field} must be of type ${fieldConfig.type}`
        );
        continue;
      }

      // ルールベースのバリデーション
      for (const rule of fieldConfig.rules) {
        const ruleError = this.validateRule(
          value,
          rule,
          field
        );
        if (ruleError) {
          fieldErrors.push(ruleError);
        }
      }

      // カスタムバリデーション
      if (fieldConfig.customValidator) {
        const customResult = fieldConfig.customValidator(
          value as T[keyof T]
        );
        if (typeof customResult === 'string') {
          fieldErrors.push(customResult);
        } else if (!customResult) {
          fieldErrors.push(
            `${field} failed custom validation`
          );
        }
      }

      // 値の変換
      if (
        fieldConfig.transform &&
        fieldErrors.length === 0
      ) {
        try {
          result[field as keyof T] =
            fieldConfig.transform(value);
        } catch (error) {
          fieldErrors.push(
            `${field} transformation failed`
          );
        }
      } else if (fieldErrors.length === 0) {
        result[field as keyof T] = value as T[keyof T];
      }

      if (fieldErrors.length > 0) {
        errors[field] = fieldErrors;
      }
    }

    const isValid = Object.keys(errors).length === 0;

    return {
      isValid,
      data: isValid ? (result as T) : undefined,
      errors,
    };
  }

  private validateType(
    value: unknown,
    expectedType: string
  ): boolean {
    switch (expectedType) {
      case 'string':
        return typeof value === 'string';
      case 'number':
        return typeof value === 'number' && !isNaN(value);
      case 'boolean':
        return typeof value === 'boolean';
      case 'array':
        return Array.isArray(value);
      case 'object':
        return (
          typeof value === 'object' &&
          value !== null &&
          !Array.isArray(value)
        );
      default:
        return true;
    }
  }

  private validateRule(
    value: unknown,
    rule: string,
    field: string
  ): string | null {
    const [ruleName, ruleValue] = rule.split(':');

    switch (ruleName) {
      case 'minLength':
        if (
          typeof value === 'string' &&
          value.length < parseInt(ruleValue, 10)
        ) {
          return `${field} must be at least ${ruleValue} characters long`;
        }
        break;

      case 'maxLength':
        if (
          typeof value === 'string' &&
          value.length > parseInt(ruleValue, 10)
        ) {
          return `${field} must be no more than ${ruleValue} characters long`;
        }
        break;

      case 'min':
        if (
          typeof value === 'number' &&
          value < parseInt(ruleValue, 10)
        ) {
          return `${field} must be at least ${ruleValue}`;
        }
        break;

      case 'max':
        if (
          typeof value === 'number' &&
          value > parseInt(ruleValue, 10)
        ) {
          return `${field} must be no more than ${ruleValue}`;
        }
        break;

      case 'email':
        if (
          typeof value === 'string' &&
          !/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(value)
        ) {
          return `${field} must be a valid email address`;
        }
        break;
    }

    return null;
  }
}

Conditional Types を活用したルール推論

Conditional Types を使用することで、型の条件に基づいて適切なバリデーションルールを自動的に推論できます。

型に基づく自動ルール推論

typescript// Conditional Types による自動ルール推論
type InferValidationRules<T> = T extends string
  ? T extends `${string}@${string}.${string}`
    ? ['email'] // メールアドレス形式の文字列
    : T extends `http${string}`
    ? ['url'] // URL形式の文字列
    : ['string'] // 通常の文字列
  : T extends number
  ? T extends `${number}.${number}`
    ? ['number', 'float'] // 小数点を含む数値
    : ['number', 'integer'] // 整数
  : T extends boolean
  ? ['boolean']
  : T extends Array<infer U>
  ? ['array', ...InferValidationRules<U>]
  : T extends Record<string, unknown>
  ? ['object']
  : ['unknown'];

// 使用例:型から自動的にルールを推論
type EmailValidationRules =
  InferValidationRules<'user@example.com'>; // ['email']
type UrlValidationRules =
  InferValidationRules<'https://example.com'>; // ['url']
type NumberValidationRules = InferValidationRules<42>; // ['number', 'integer']

高度な条件分岐による動的バリデーション

typescript// より複雑な条件分岐を使った動的バリデーション
type SmartValidationConfig<T> = {
  [K in keyof T]: T[K] extends string
    ? {
        type: 'string';
        rules: T[K] extends `${string}@${string}`
          ? ['required', 'email']
          : T[K] extends `http${string}`
          ? ['required', 'url']
          : ['required', 'minLength:1'];
        pattern?: T[K] extends `${string}@${string}`
          ? RegExp
          : never;
      }
    : T[K] extends number
    ? {
        type: 'number';
        rules: ['required', 'positive'];
        min?: number;
        max?: number;
      }
    : T[K] extends boolean
    ? {
        type: 'boolean';
        rules: ['required'];
      }
    : T[K] extends Array<infer U>
    ? {
        type: 'array';
        rules: ['required'];
        itemValidation: SmartValidationConfig<U>;
      }
    : {
        type: 'object';
        rules: ['required'];
        properties: T[K] extends Record<string, unknown>
          ? SmartValidationConfig<T[K]>
          : never;
      };
};

// 実装例
interface SmartForm {
  email: 'user@example.com';
  website: 'https://example.com';
  age: 25;
  isActive: true;
  tags: string[];
  profile: {
    name: string;
    bio: string;
  };
}

// 自動生成される高度なバリデーション設定
type SmartFormValidation = SmartValidationConfig<SmartForm>;

Decorator パターンによるメタデータ収集

Decorator を使用することで、クラスのプロパティに直接バリデーションルールを定義し、メタデータとして収集できます。

バリデーション Decorator の実装

typescript// バリデーション用のメタデータ型定義
interface ValidationMetadata {
  propertyKey: string;
  rules: string[];
  type: string;
  required: boolean;
  customValidator?: (value: unknown) => boolean | string;
}

// メタデータストレージ
const validationMetadataMap = new WeakMap<
  Function,
  ValidationMetadata[]
>();

// バリデーション Decorator の実装
function Validate(
  rules: string[],
  options?: {
    required?: boolean;
    customValidator?: (value: unknown) => boolean | string;
  }
) {
  return function (target: any, propertyKey: string) {
    const constructor = target.constructor;
    const existingMetadata =
      validationMetadataMap.get(constructor) || [];

    const metadata: ValidationMetadata = {
      propertyKey,
      rules,
      type:
        Reflect.getMetadata(
          'design:type',
          target,
          propertyKey
        )?.name.toLowerCase() || 'unknown',
      required: options?.required ?? true,
      customValidator: options?.customValidator,
    };

    validationMetadataMap.set(constructor, [
      ...existingMetadata,
      metadata,
    ]);
  };
}

// 特定用途の Decorator
function Required() {
  return Validate(['required']);
}

function Email() {
  return Validate(['required', 'email']);
}

function MinLength(length: number) {
  return Validate([`minLength:${length}`]);
}

function Range(min: number, max: number) {
  return Validate([`min:${min}`, `max:${max}`]);
}

// 使用例
class UserRegistrationForm {
  @Required()
  @MinLength(3)
  username!: string;

  @Email()
  email!: string;

  @Range(18, 120)
  age!: number;

  @Validate(['required'], {
    customValidator: (value) => {
      if (typeof value !== 'string') return false;
      return (
        value.length >= 8 &&
        /[A-Z]/.test(value) &&
        /[0-9]/.test(value)
      );
    },
  })
  password!: string;

  @Validate(['optional'])
  bio?: string;
}

Decorator メタデータを活用したバリデーター生成

typescript// Decorator メタデータを使用したバリデーター生成クラス
class DecoratorValidator {
  static createValidator<T>(constructor: new () => T): (
    data: unknown
  ) => {
    isValid: boolean;
    data?: T;
    errors: Record<string, string[]>;
  } {
    const metadata =
      validationMetadataMap.get(constructor) || [];

    return (data: unknown) => {
      const errors: Record<string, string[]> = {};

      if (typeof data !== 'object' || data === null) {
        return {
          isValid: false,
          errors: { _root: ['Data must be an object'] },
        };
      }

      const obj = data as Record<string, unknown>;
      const result: Partial<T> = {};

      for (const meta of metadata) {
        const fieldErrors: string[] = [];
        const value = obj[meta.propertyKey];

        // 必須チェック
        if (
          meta.required &&
          (value === undefined || value === null)
        ) {
          fieldErrors.push(
            `${meta.propertyKey} is required`
          );
          continue;
        }

        // オプショナルで値が未定義の場合はスキップ
        if (
          !meta.required &&
          (value === undefined || value === null)
        ) {
          continue;
        }

        // 型チェック
        if (!this.validateType(value, meta.type)) {
          fieldErrors.push(
            `${meta.propertyKey} must be of type ${meta.type}`
          );
          continue;
        }

        // ルールベースのバリデーション
        for (const rule of meta.rules) {
          const ruleError = this.validateRule(
            value,
            rule,
            meta.propertyKey
          );
          if (ruleError) {
            fieldErrors.push(ruleError);
          }
        }

        // カスタムバリデーション
        if (meta.customValidator) {
          const customResult = meta.customValidator(value);
          if (typeof customResult === 'string') {
            fieldErrors.push(customResult);
          } else if (!customResult) {
            fieldErrors.push(
              `${meta.propertyKey} failed custom validation`
            );
          }
        }

        if (fieldErrors.length === 0) {
          result[meta.propertyKey as keyof T] =
            value as T[keyof T];
        } else {
          errors[meta.propertyKey] = fieldErrors;
        }
      }

      const isValid = Object.keys(errors).length === 0;

      return {
        isValid,
        data: isValid ? (result as T) : undefined,
        errors,
      };
    };
  }

  private validateType(
    value: unknown,
    expectedType: string
  ): boolean {
    switch (expectedType) {
      case 'string':
        return typeof value === 'string';
      case 'number':
        return typeof value === 'number' && !isNaN(value);
      case 'boolean':
        return typeof value === 'boolean';
      default:
        return true;
    }
  }

  private validateRule(
    value: unknown,
    rule: string,
    field: string
  ): string | null {
    const [ruleName, ruleValue] = rule.split(':');

    switch (ruleName) {
      case 'required':
        return null; // 既に必須チェックは完了

      case 'email':
        if (
          typeof value === 'string' &&
          !/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(value)
        ) {
          return `${field} must be a valid email address`;
        }
        break;

      case 'minLength':
        if (
          typeof value === 'string' &&
          value.length < parseInt(ruleValue, 10)
        ) {
          return `${field} must be at least ${ruleValue} characters long`;
        }
        break;

      case 'min':
        if (
          typeof value === 'number' &&
          value < parseInt(ruleValue, 10)
        ) {
          return `${field} must be at least ${ruleValue}`;
        }
        break;

      case 'max':
        if (
          typeof value === 'number' &&
          value > parseInt(ruleValue, 10)
        ) {
          return `${field} must be no more than ${ruleValue}`;
        }
        break;
    }

    return null;
  }
}

AST 解析による型情報の実行時活用

TypeScript の抽象構文木(AST)を解析することで、コンパイル時の型情報を実行時に活用できる高度な手法を実装できます。この手法により、最も包括的で自動化されたバリデーション生成が可能になります。

TypeScript Compiler API を活用した型情報抽出

typescriptimport * as ts from 'typescript';
import * as fs from 'fs';

// AST解析による型情報抽出クラス
class TypeScriptASTAnalyzer {
  private program: ts.Program;
  private checker: ts.TypeChecker;

  constructor(
    filePaths: string[],
    compilerOptions?: ts.CompilerOptions
  ) {
    this.program = ts.createProgram(
      filePaths,
      compilerOptions || {
        target: ts.ScriptTarget.ES2020,
        module: ts.ModuleKind.CommonJS,
        strict: true,
        experimentalDecorators: true,
        emitDecoratorMetadata: true,
      }
    );
    this.checker = this.program.getTypeChecker();
  }

  // インターフェースから型情報を抽出
  extractInterfaceInfo(
    interfaceName: string
  ): ValidationSchema | null {
    for (const sourceFile of this.program.getSourceFiles()) {
      if (sourceFile.isDeclarationFile) continue;

      const interfaceDeclaration = this.findInterface(
        sourceFile,
        interfaceName
      );
      if (interfaceDeclaration) {
        return this.analyzeInterface(interfaceDeclaration);
      }
    }
    return null;
  }

  private findInterface(
    sourceFile: ts.SourceFile,
    name: string
  ): ts.InterfaceDeclaration | null {
    let result: ts.InterfaceDeclaration | null = null;

    function visit(node: ts.Node) {
      if (
        ts.isInterfaceDeclaration(node) &&
        node.name.text === name
      ) {
        result = node;
        return;
      }
      ts.forEachChild(node, visit);
    }

    visit(sourceFile);
    return result;
  }

  private analyzeInterface(
    interfaceDeclaration: ts.InterfaceDeclaration
  ): ValidationSchema {
    const schema: ValidationSchema = {};

    for (const member of interfaceDeclaration.members) {
      if (
        ts.isPropertySignature(member) &&
        member.name &&
        ts.isIdentifier(member.name)
      ) {
        const propertyName = member.name.text;
        const type = this.checker.getTypeAtLocation(member);
        const isOptional = !!member.questionToken;

        schema[propertyName] = {
          type: this.getTypeString(type),
          required: !isOptional,
          rules: this.inferRulesFromType(type),
          nullable: this.isNullableType(type),
        };
      }
    }

    return schema;
  }

  private getTypeString(type: ts.Type): string {
    if (type.flags & ts.TypeFlags.String) return 'string';
    if (type.flags & ts.TypeFlags.Number) return 'number';
    if (type.flags & ts.TypeFlags.Boolean) return 'boolean';
    if (type.flags & ts.TypeFlags.Object) {
      const objectType = type as ts.ObjectType;
      if (
        objectType.objectFlags & ts.ObjectFlags.Reference
      ) {
        const typeRef = objectType as ts.TypeReference;
        if (typeRef.target.symbol?.name === 'Array') {
          return 'array';
        }
      }
      return 'object';
    }
    if (type.flags & ts.TypeFlags.Union) {
      const unionType = type as ts.UnionType;
      const types = unionType.types.map((t) =>
        this.getTypeString(t)
      );
      return types.join(' | ');
    }
    return 'unknown';
  }

  private inferRulesFromType(type: ts.Type): string[] {
    const rules: string[] = [];

    // 文字列リテラル型からルールを推論
    if (type.flags & ts.TypeFlags.StringLiteral) {
      const literalType = type as ts.StringLiteralType;
      const value = literalType.value;

      if (value.includes('@')) {
        rules.push('email');
      } else if (value.startsWith('http')) {
        rules.push('url');
      }
    }

    // ユニオン型からルールを推論
    if (type.flags & ts.TypeFlags.Union) {
      const unionType = type as ts.UnionType;
      const literalValues = unionType.types
        .filter((t) => t.flags & ts.TypeFlags.StringLiteral)
        .map((t) => (t as ts.StringLiteralType).value);

      if (literalValues.length > 0) {
        rules.push(`enum:${literalValues.join(',')}`);
      }
    }

    // 数値型からルールを推論
    if (type.flags & ts.TypeFlags.Number) {
      rules.push('numeric');
    }

    return rules;
  }

  private isNullableType(type: ts.Type): boolean {
    if (type.flags & ts.TypeFlags.Union) {
      const unionType = type as ts.UnionType;
      return unionType.types.some(
        (t) =>
          t.flags & ts.TypeFlags.Null ||
          t.flags & ts.TypeFlags.Undefined
      );
    }
    return false;
  }
}

// バリデーションスキーマの型定義
interface ValidationSchema {
  [key: string]: {
    type: string;
    required: boolean;
    rules: string[];
    nullable: boolean;
  };
}

AST 解析結果を活用したバリデーター生成

typescript// AST解析結果を使用したバリデーター生成
class ASTBasedValidator {
  private analyzer: TypeScriptASTAnalyzer;
  private validatorCache: Map<string, Function> = new Map();

  constructor(sourceFiles: string[]) {
    this.analyzer = new TypeScriptASTAnalyzer(sourceFiles);
  }

  // インターフェース名からバリデーターを生成
  createValidatorFromInterface<T>(
    interfaceName: string
  ): (data: unknown) => ValidationResult<T> {
    // キャッシュから取得を試行
    if (this.validatorCache.has(interfaceName)) {
      return this.validatorCache.get(interfaceName) as (
        data: unknown
      ) => ValidationResult<T>;
    }

    const schema =
      this.analyzer.extractInterfaceInfo(interfaceName);
    if (!schema) {
      throw new Error(
        `Interface ${interfaceName} not found`
      );
    }

    const validator = this.generateValidator<T>(schema);
    this.validatorCache.set(interfaceName, validator);

    return validator;
  }

  private generateValidator<T>(
    schema: ValidationSchema
  ): (data: unknown) => ValidationResult<T> {
    return (data: unknown): ValidationResult<T> => {
      const errors: Record<string, string[]> = {};

      if (typeof data !== 'object' || data === null) {
        return {
          isValid: false,
          errors: { _root: ['Data must be an object'] },
        };
      }

      const obj = data as Record<string, unknown>;
      const result: Partial<T> = {};

      for (const [fieldName, fieldSchema] of Object.entries(
        schema
      )) {
        const fieldErrors: string[] = [];
        const value = obj[fieldName];

        // 必須チェック
        if (
          fieldSchema.required &&
          (value === undefined || value === null)
        ) {
          fieldErrors.push(`${fieldName} is required`);
          continue;
        }

        // null許可チェック
        if (value === null && !fieldSchema.nullable) {
          fieldErrors.push(`${fieldName} cannot be null`);
          continue;
        }

        // 値が存在する場合のバリデーション
        if (value !== undefined && value !== null) {
          // 型チェック
          if (!this.validateType(value, fieldSchema.type)) {
            fieldErrors.push(
              `${fieldName} must be of type ${fieldSchema.type}`
            );
            continue;
          }

          // ルールベースのバリデーション
          for (const rule of fieldSchema.rules) {
            const ruleError = this.validateRule(
              value,
              rule,
              fieldName
            );
            if (ruleError) {
              fieldErrors.push(ruleError);
            }
          }
        }

        if (
          fieldErrors.length === 0 &&
          value !== undefined
        ) {
          result[fieldName as keyof T] =
            value as T[keyof T];
        } else if (fieldErrors.length > 0) {
          errors[fieldName] = fieldErrors;
        }
      }

      const isValid = Object.keys(errors).length === 0;

      return {
        isValid,
        data: isValid ? (result as T) : undefined,
        errors,
      };
    };
  }

  private validateType(
    value: unknown,
    expectedType: string
  ): boolean {
    if (expectedType.includes(' | ')) {
      // ユニオン型の場合
      const types = expectedType.split(' | ');
      return types.some((type) =>
        this.validateType(value, type.trim())
      );
    }

    switch (expectedType) {
      case 'string':
        return typeof value === 'string';
      case 'number':
        return typeof value === 'number' && !isNaN(value);
      case 'boolean':
        return typeof value === 'boolean';
      case 'array':
        return Array.isArray(value);
      case 'object':
        return (
          typeof value === 'object' &&
          value !== null &&
          !Array.isArray(value)
        );
      default:
        return true;
    }
  }

  private validateRule(
    value: unknown,
    rule: string,
    field: string
  ): string | null {
    const [ruleName, ruleValue] = rule.split(':');

    switch (ruleName) {
      case 'email':
        if (
          typeof value === 'string' &&
          !/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(value)
        ) {
          return `${field} must be a valid email address`;
        }
        break;

      case 'url':
        if (
          typeof value === 'string' &&
          !/^https?:\/\/.+/.test(value)
        ) {
          return `${field} must be a valid URL`;
        }
        break;

      case 'enum':
        if (ruleValue) {
          const allowedValues = ruleValue.split(',');
          if (!allowedValues.includes(String(value))) {
            return `${field} must be one of: ${allowedValues.join(
              ', '
            )}`;
          }
        }
        break;

      case 'numeric':
        if (typeof value !== 'number' || isNaN(value)) {
          return `${field} must be a valid number`;
        }
        break;
    }

    return null;
  }
}

// バリデーション結果の型定義
interface ValidationResult<T> {
  isValid: boolean;
  data?: T;
  errors: Record<string, string[]>;
}

実際の使用例とビルドプロセス統合

typescript// 実際のプロジェクトでの使用例
interface ApiUser {
  id: number;
  username: string;
  email: string;
  role: 'admin' | 'user' | 'guest';
  profile?: {
    firstName: string;
    lastName: string;
    avatar?: string;
  };
  createdAt: string;
  isActive: boolean;
}

// AST解析ベースのバリデーター使用
const validator = new ASTBasedValidator([
  './types/user.ts',
]);
const validateApiUser =
  validator.createValidatorFromInterface<ApiUser>(
    'ApiUser'
  );

// API レスポンスのバリデーション
async function fetchUser(userId: number): Promise<ApiUser> {
  const response = await fetch(`/api/users/${userId}`);
  const data = await response.json();

  const validationResult = validateApiUser(data);

  if (!validationResult.isValid) {
    throw new Error(
      `Invalid user data: ${JSON.stringify(
        validationResult.errors
      )}`
    );
  }

  return validationResult.data!;
}

// ビルド時バリデーター生成スクリプト
class BuildTimeValidatorGenerator {
  static generateValidators(
    sourceFiles: string[],
    outputPath: string,
    interfaces: string[]
  ): void {
    const analyzer = new TypeScriptASTAnalyzer(sourceFiles);
    const validators: Record<string, ValidationSchema> = {};

    for (const interfaceName of interfaces) {
      const schema =
        analyzer.extractInterfaceInfo(interfaceName);
      if (schema) {
        validators[interfaceName] = schema;
      }
    }

    const validatorCode =
      this.generateValidatorCode(validators);
    fs.writeFileSync(outputPath, validatorCode);
  }

  private static generateValidatorCode(
    validators: Record<string, ValidationSchema>
  ): string {
    return `
// Auto-generated validators
${Object.entries(validators)
  .map(
    ([name, schema]) => `
export const validate${name} = (data: unknown): ValidationResult<${name}> => {
  // Generated validation logic based on schema
  ${JSON.stringify(schema, null, 2)}
};
`
  )
  .join('\n')}
    `;
  }
}

まとめ:型駆動開発の実践指針と将来展望

TypeScript の型システムを活用した実行時バリデーション自動生成は、従来の手動実装が抱える課題を根本的に解決する革新的なアプローチです。本記事で紹介した各手法の特徴と適用場面をまとめ、実践的な指針を提示します。

各手法の比較と選択指針

実際のプロジェクトでどの手法を選択すべきかを判断するための比較表を示します。

#手法実装難易度型安全性パフォーマンス保守性適用場面
1Template Literal Types動的なルール組み合わせが必要
2Mapped Types非常に高非常に高既存型からの自動生成を重視
3Conditional Types非常に高複雑な条件分岐ロジックが必要
4Decorator パターンクラスベースの設計を採用
5AST 解析非常に高最高最高最大限の自動化と型安全性を追求

実践的な導入戦略

段階的導入アプローチ

typescript// Phase 1: 基本的なMapped Typesから開始
type BasicValidation<T> = {
  [K in keyof T]: {
    required: boolean;
    type: string;
  };
};

// Phase 2: Template Literal Typesでルール拡張
type EnhancedValidation<T> = {
  [K in keyof T]: {
    required: boolean;
    type: string;
    rules: string[];
  };
};

// Phase 3: Conditional Typesで高度な推論を追加
type SmartValidation<T> = {
  [K in keyof T]: T[K] extends string
    ? { type: 'string'; rules: string[]; pattern?: RegExp }
    : T[K] extends number
    ? {
        type: 'number';
        rules: string[];
        range?: [number, number];
      }
    : { type: 'unknown'; rules: string[] };
};

// Phase 4: AST解析による完全自動化
// ビルドプロセスに統合して最大限の自動化を実現

プロジェクト規模別の推奨手法

typescript// 小規模プロジェクト(~10 インターフェース)
// → Decorator パターンまたは基本的な Mapped Types

// 中規模プロジェクト(10~50 インターフェース)
// → Template Literal Types + Mapped Types の組み合わせ

// 大規模プロジェクト(50+ インターフェース)
// → AST 解析による完全自動化 + ビルドプロセス統合

パフォーマンス最適化のベストプラクティス

バリデーター生成の最適化

typescript// バリデーターキャッシュによる最適化
class OptimizedValidatorFactory {
  private static cache = new Map<string, Function>();

  static getValidator<T>(
    key: string,
    generator: () => Function
  ): (data: unknown) => ValidationResult<T> {
    if (!this.cache.has(key)) {
      this.cache.set(key, generator());
    }
    return this.cache.get(key) as (
      data: unknown
    ) => ValidationResult<T>;
  }
}

// 使用例
const validateUser = OptimizedValidatorFactory.getValidator(
  'User',
  () => createUserValidator()
);

実行時最適化テクニック

typescript// 早期リターンによる最適化
function optimizedValidate<T>(
  data: unknown,
  schema: ValidationSchema
): ValidationResult<T> {
  // 基本的な型チェックで早期リターン
  if (typeof data !== 'object' || data === null) {
    return {
      isValid: false,
      errors: { _root: ['Invalid data type'] },
    };
  }

  // 必須フィールドの事前チェック
  const obj = data as Record<string, unknown>;
  const requiredFields = Object.entries(schema)
    .filter(([, config]) => config.required)
    .map(([field]) => field);

  for (const field of requiredFields) {
    if (!(field in obj)) {
      return {
        isValid: false,
        errors: { [field]: [`${field} is required`] },
      };
    }
  }

  // 詳細なバリデーションを実行
  return detailedValidate(data, schema);
}

将来展望と技術トレンド

TypeScript の進化と新機能活用

TypeScript の継続的な進化により、さらに高度な型レベルプログラミングが可能になっています。

typescript// TypeScript 5.0+ の新機能を活用した将来的な実装例
type FutureValidation<T> = {
  [K in keyof T as T[K] extends Function ? never : K]: {
    type: TypeToString<T[K]>;
    constraints: InferConstraints<T[K]>;
    metadata: ExtractMetadata<T[K]>;
  };
};

// より高度な型推論による自動バリデーション
type AutoInferredValidator<T> = T extends Record<
  string,
  unknown
>
  ? { [K in keyof T]: AutoValidationRule<T[K]> }
  : never;

エコシステムとの統合

typescript// React Hook Form との統合例
function useTypedForm<T>(schema: ValidationSchema<T>) {
  const validator = useMemo(
    () => createValidator(schema),
    [schema]
  );

  return useForm<T>({
    resolver: (data) => {
      const result = validator(data);
      return {
        values: result.isValid ? result.data : {},
        errors: result.errors,
      };
    },
  });
}

// GraphQL との統合例
function createGraphQLValidator<T>(typeDefs: string) {
  const schema = parseGraphQLSchema(typeDefs);
  return createValidatorFromGraphQLSchema<T>(schema);
}

開発チームでの導入ガイドライン

コーディング規約の策定

typescript// チーム内での統一ルール例
namespace ValidationStandards {
  // 1. インターフェース定義時は必ずバリデーション考慮
  export interface StandardInterface {
    // JSDoc でバリデーションルールを明記
    /** @validation required,email */
    email: string;

    /** @validation required,min:1,max:100 */
    name: string;

    /** @validation optional,positive */
    age?: number;
  }

  // 2. バリデーター命名規則
  export const validateStandardInterface =
    createValidator<StandardInterface>();

  // 3. エラーハンドリングの統一
  export function handleValidationError(
    errors: Record<string, string[]>
  ): void {
    // 統一されたエラー処理ロジック
  }
}

TypeScript の型システムを活用した実行時バリデーション自動生成は、現代の Web 開発における品質向上と開発効率化の両立を実現する重要な技術です。適切な手法選択と段階的な導入により、より堅牢で保守性の高いアプリケーション開発が可能になります。

今後も TypeScript の進化とともに、さらに高度で実用的な手法が登場することが期待されます。継続的な学習と実践を通じて、型駆動開発の恩恵を最大限に活用していきましょう。

関連リンク