TypeScript declare の実務活用ガイド|型定義で外部ライブラリを安全に扱う

TypeScript

TypeScript declare の実務活用ガイド|型定義で外部ライブラリを安全に扱う

はじめに

TypeScript を使う開発現場で、しばしば「型定義がない外部ライブラリ」や「実行時に存在するグローバルオブジェクト」と遭遇します。そんなとき活躍するのが declare キーワードです。本記事では、declare の実務的な使い方を、実際の業務シーンを想定したコード例を交えて解説します。

declare キーワードの基礎

declare はTypeScript特有のキーワードで、「このコードは実行時に存在するが、TypeScriptコンパイル時には定義がない」という状況を型チェッカーに伝えるためのものです。JavaScript のコードは実行されず、型定義のみがTypeScriptに認識されます。

基本的な使い方は以下の通りです:

// グローバル変数を declare する
declare const API_KEY: string;

// 関数を declare する
declare function initializeSDK(config: object): void;

// クラスを declare する
declare class PaymentGateway {
  charge(amount: number): Promise<string>;
}

// モジュール全体を declare する
declare module 'analytics-library' {
  export function trackEvent(eventName: string, data: object): void;
}

これらは実装を持たず、型情報のみをTypeScriptに提供します。

業務でのユースケース

1. サードパーティSDKの型定義が不完全または存在しない場合

決済ゲートウェイやアナリティクスツールなど、型定義ファイル(.d.ts)が提供されていないライブラリは珍しくありません。そのような場合に declare を使用します。

2. Window オブジェクトへの動的なプロパティ追加

グローバルスコープに実行時に追加されるプロパティがある場合、TypeScriptには事前に知らせる必要があります。

3. レガシーコードベースとの統合

既存のJavaScriptコードと TypeScript を段階的に統合する際、外部スクリプトで定義されたグローバル変数を安全に扱う必要があります。

実装コード例

実務例1:決済ゲートウェイの型定義

実際のプロジェクトでよく見かけるシナリオです。Stripe や Braintree などの決済SDKは、グローバルスコープに自身を登録します。

// types/payment.d.ts
declare global {
  interface Window {
    Stripe: StripeConstructor;
  }
}

interface StripeConstructor {
  (publicKey: string): StripeInstance;
}

interface StripeInstance {
  createPaymentMethod(options: PaymentMethodOptions): Promise<PaymentMethodResult>;
  confirmCardPayment(clientSecret: string, data: PaymentData): Promise<ConfirmResult>;
}

interface PaymentMethodOptions {
  type: 'card';
  card: { token: string };
  billing_details?: BillingDetails;
}

interface BillingDetails {
  name?: string;
  email?: string;
  phone?: string;
  address?: AddressInfo;
}

interface AddressInfo {
  postal_code: string;
  country: string;
}

interface PaymentMethodResult {
  paymentMethod?: { id: string };
  error?: { message: string };
}

interface PaymentData {
  payment_method: string;
  metadata?: Record<string, any>;
}

interface ConfirmResult {
  paymentIntent?: { status: string; id: string };
  error?: { message: string };
}

export {};

このファイルをプロジェクトに置くと、TypeScript はこれらの型定義を認識します。

// src/services/paymentService.ts
import type { StripeInstance } from '../types/payment';

export class PaymentService {
  private stripe: StripeInstance;

  constructor(publicKey: string) {
    // Stripe スクリプトはHTMLで読み込み済みと想定
    this.stripe = window.Stripe(publicKey);
  }

  async processPayment(token: string, amount: number) {
    try {
      const paymentMethod = await this.stripe.createPaymentMethod({
        type: 'card',
        card: { token },
        billing_details: {
          name: 'John Doe',
          email: 'john@example.com',
          address: {
            postal_code: '12345',
            country: 'US'
          }
        }
      });

      if (paymentMethod.error) {
        throw new Error(paymentMethod.error.message);
      }

      return paymentMethod.paymentMethod?.id;
    } catch (error) {
      console.error('Payment processing failed:', error);
      throw error;
    }
  }

  async confirmPayment(clientSecret: string, paymentMethodId: string) {
    const result = await this.stripe.confirmCardPayment(clientSecret, {
      payment_method: paymentMethodId,
      metadata: {
        timestamp: new Date().toISOString()
      }
    });

    if (result.error) {
      throw new Error(result.error.message);
    }

    return result.paymentIntent;
  }
}

実務例2:アナリティクスライブラリの統合

多くのアナリティクスツールはグローバルキューを使用するパターンです。

// types/analytics.d.ts
declare global {
  interface Window {
    gtag: (command: string, action: string, params?: Record<string, any>) => void;
    dataLayer?: Record<string, any>[];
  }
}

declare const gtag: (command: string, action: string, params?: Record<string, any>) => void;

export {};
// src/services/analyticsService.ts
export class AnalyticsService {
  /**
   * ページビューを記録
   */
  static pageView(pagePath: string, pageTitle: string) {
    if (typeof window !== 'undefined' && window.gtag) {
      gtag('config', 'GA_MEASUREMENT_ID', {
        page_path: pagePath,
        page_title: pageTitle,
        timestamp: new Date().getTime()
      });
    }
  }

  /**
   * カスタムイベントを記録(購入完了など)
   */
  static trackPurchase(
    transactionId: string,
    value: number,
    currency: string,
    items: Array<{ id: string; name: string; price: number }>
  ) {
    if (typeof window !== 'undefined' && window.gtag) {
      gtag('event', 'purchase', {
        transaction_id: transactionId,
        value,
        currency,
        items: items.map(item => ({
          item_id: item.id,
          item_name: item.name,
          price: item.price
        }))
      });
    }
  }

  /**
   * ユーザーIDを設定
   */
  static setUserId(userId: string) {
    if (typeof window !== 'undefined' && window.gtag) {
      gtag('config', 'GA_MEASUREMENT_ID', {
        user_id: userId
      });
    }
  }

  /**
   * カスタム属性を設定
   */
  static setUserProperties(properties: Record<string, string | number>) {
    if (typeof window !== 'undefined' && window.gtag) {
      gtag('set', 'user_properties', properties);
    }
  }
}

実務例3:複雑なモジュール型定義

複数の機能を持つライブラリの場合、モジュール全体を declare することもあります。

// types/custom-logger.d.ts
declare module 'custom-logger' {
  interface LogContext {
    userId?: string;
    sessionId?: string;
    environment?: 'development' | 'production' | 'staging';
    [key: string]: any;
  }

  interface LoggerConfig {
    level: 'debug' | 'info' | 'warn' | 'error';
    format: 'json' | 'text';
    transporters: Array<{
      type: 'console' | 'file' | 'remote';
      url?: string;
      path?: string;
    }>;
  }

  class Logger {
    constructor(config: LoggerConfig);
    setContext(context: LogContext): void;
    debug(message: string, data?: any): void;
    info(message: string, data?: any): void;
    warn(message: string, data?: any): void;
    error(message: string, error?: Error | string, data?: any): void;
    flush(): Promise<void>;
  }

  export = Logger;
}
// src/utils/logger.ts
import Logger from 'custom-logger';

export const createLogger = (environment: string) => {
  const logger = new Logger({
    level: environment === 'production' ? 'warn' : 'debug',
    format: 'json',
    transporters: [
      {
        type: 'console'
      },
      ...(environment === 'production'
        ? [
            {
              type: 'remote',
              url: 'https://logs.example.com/api/logs'
            }
          ]
        : [])
    ]
  });

  logger.setContext({
    environment: environment as any,
    timestamp: new Date().toISOString()
  });

  return logger;
};

// 使用例
const logger = createLogger(process.env.NODE_ENV || 'development');

export const logUserAction = async (userId: string, action: string, details?: any) => {
  logger.setContext({
    userId,
    sessionId: sessionStorage.getItem('sessionId') || undefined
  });

  logger.info(`User action: ${action}`, {
    action,
    timestamp: new Date().toISOString(),
    ...details
  });

  // 本番環境ではログをリモートに送信
  if (process.env.NODE_ENV === 'production') {
    await logger.flush();
  }
};

よくある応用パターン

パターン1:条件付き型定義

環境に応じて異なるグローバル変数を定義する必要がある場合があります。

// types/environment.d.ts
declare global {
  interface Window {
    __DEV__: boolean;
    __API_BASE_URL__: string;
  }

  namespace NodeJS {
    interface ProcessEnv {
      REACT_APP_API_BASE: string;
      REACT_APP_STRIPE_KEY: string;
      REACT_APP_ANALYTICS_ID: string;
    }
  }
}

// ビルド時に注入される値
declare const __VERSION__: string;
declare const __BUILD_TIME__: string;

export {};
// src/config.ts
export const getConfig = () => {
  return {
    isDevelopment: window.__DEV__,
    apiBaseUrl: window.__API_BASE_URL__,
    version: __VERSION__,
    buildTime: __BUILD_TIME__,
    stripeKey: process.env.REACT_APP_STRIPE_KEY,
    analyticsId: process.env.REACT_APP_ANALYTICS_ID
  };
};

パターン2:名前空間を使った整理

複数の関連する型定義をグループ化する場合、名前空間が便利です。

// types/thirdparty.d.ts\ndeclare global {\n  namespace ThirdPartyAPIs {\n    interface FacebookSDK {\n      init(config: FacebookConfig): void;\n      track(event: string, data: object): void;\n    }\n\n    interface LinkedInInsight {\n      track(conversions: ConversionData): void;\n    }\n\n    interface FacebookConfig {\n      appId: string;\n      version: string;\n    }\n\n    interface ConversionData {\n      conversionId: string;\n      value: number;\n      currency: string;\n    }\n  }\n\n  interface Window {\n    fbq: ThirdPartyAPIs.FacebookSDK;\n    lintrk: ThirdPartyAPIs.LinkedInInsight;\n  }\n}\n\nexport {};

パターン3:型安全なイベントエミッター

グローバルイベントを型安全に扱う実務的なパターンです。

// types/events.d.ts\ndeclare global {\n  interface GlobalEventMap {\n    'user:login': { userId: string; timestamp: Date };\n    'user:logout': { userId: string };\n    'cart:updated': { itemCount: number; totalPrice: number };\n    'error:occurred': { message: string; code: string; context?: object };\n  }\n\n  interface Window {\n    __eventBus: EventBus;\n  }\n}\n\ninterface EventBus {\n  on<K extends keyof GlobalEventMap>(\n    event: K,\n    callback: (data: GlobalEventMap[K]) => void\n  ): void;\n  emit<K extends keyof GlobalEventMap>(\n    event: K,\n    data: GlobalEventMap[K]\n  ): void;\n}\n\nexport {};
// src/eventBus.ts\nclass TypeSafeEventBus implements EventBus {\n  private listeners: Map<string, Set<Function>> = new Map();\n\n  on<K extends keyof GlobalEventMap>(\n    event: K,\n    callback: (data: GlobalEventMap[K]) => void\n  ) {\n    if (!this.listeners.has(event as string)) {\n      this.listeners.set(event as string, new Set());\n    }\n    this.listeners.get(event as string)!.add(callback);\n  }\n\n  emit<K extends keyof GlobalEventMap>(\n    event: K,\n    data: GlobalEventMap[K]\n  ) {\n    const callbacks = this.listeners.get(event as string);\n    if (callbacks) {\n      callbacks.forEach(callback => callback(data));\n    }\n  }\n}\n\nif (typeof window !== 'undefined') {\n  window.__eventBus = new TypeSafeEventBus();\n}\n\n// 使用例\nwindow.__eventBus.on('user:login', (data) => {\n  // data は { userId: string; timestamp: Date } として型推論される\n  console.log(`User ${data.userId} logged in at ${data.timestamp}`);\n});\n\nwindow.__eventBus.emit('user:login', {\n  userId: '12345',\n  timestamp: new Date()\n});

注意点とベストプラクティス

1. declare は実装ではなく型宣言のみ

よくある間違いは、declare 内に実装を書こうとすることです。declare はコンパイル時にしか存在しないため、実装は別に必要です。

// ❌ 間違い:実装を含む
declare function myFunction(x: number) {
  return x * 2;
}\n\n// ✅ 正しい:型定義のみ\ndeclare function myFunction(x: number): number;

2. declare global の使い方に注意

declare global を複数のファイルで使う場合、各ファイルの最後に export {} を記載してモジュールとして認識させましょう。

// types/globals.d.ts\ndeclare global {\n  interface Window {\n    myGlobal: string;\n  }\n}\n\n// このエクスポートがないと、TypeScript がモジュールと認識しない場合がある\nexport {};

3. 実行時の存在確認

declare はTypeScriptにのみ伝えるもので、実行時に実際に存在するかは保証されません。必ずガード句を使いましょう。

// ❌ 危険:declare したから必ず存在すると仮定\nwindow.myFunction();\n\n// ✅ 安全:実行時に確認\nif (typeof window !== 'undefined' && window.myFunction) {\n  window.myFunction();\n}

4. 型定義ファイルの場所

プロジェクトルートに types フォルダを作成し、declare はそこに集約するのが慣例です。tsconfig.json で参照パスを設定します。

// tsconfig.json\n{\n  \"compilerOptions\": {\n    \"typeRoots\": [\"./node_modules/@types\", \"./types\"],\n    \"baseUrl\": \".\",\n    \"paths\": {\n      \"@types/*\": [\"types/*\"]\n    }\n  },\n  \"include\": [\"src/**/*\", \"types/**/*.d.ts\"]\n}

5. @types パッケージの優先

サードパーティライブラリの型定義がある場合、npm の @types スコープのパッケージが存在しないか確認してください。わざわざ declare で定義するより、既存の型定義を使う方が保守性が高いです。

npm search @types/lodash
npm install --save-dev @types/lodash

6. Any の回避

declare は型定義を簡略化するために使いがちですが、 any を多用すると TypeScript の利点が失われます。可能な限り具体的な型を定義しましょう。

// ❌ 避けるべき\ndeclare const api: any;\n\n// ✅ 具体的に定義\ndeclare const api: {\n  get(url: string): Promise<Response>;\n  post(url: string, data: object): Promise<Response>;\n};

まとめ

TypeScript の declare キーワードは、型定義がない外部ライブラリやグローバルオブジェクトを安全に扱うための重要な機能です。実務では以下の3つのシーンで頻出します。

  • サードパーティSDK:決済ゲートウェイやアナリティクスツールの型定義
  • グローバルスコープの拡張:Window オブジェクトへの属性追加
  • レガシーコード統合:既存JavaScriptとTypeScriptの共存

実装時の重要ポイントは:

  • declare は型宣言のみ、実装ではない
  • 必ず実行時に存在確認を行う
  • 型定義は可能な限り具体的に
  • declare global は export {} でモジュール化
  • 既存の @types パッケージがないか先に確認

正しく使うことで、型安全性を保ちながら、JavaScriptの柔軟性を活かした開発ができます。業務コードでの declare 活用は、チーム全体の開発効率と保守性を大きく向上させるでしょう。

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