TypeScriptの型宣言ファイル設計パターン：グローバル宣言とモジュール宣言の共存


## はじめに

TypeScriptで大規模なWebアプリケーションを開発していると、さまざまな種類の型宣言が必要になります。特に次のような場面でよく悩みます：

- Pugなどのテンプレートファイルのインポート
- Windowオブジェクトへのカスタムプロパティの追加
- グローバル関数や変数の型付け

これらの型宣言を一つのファイルにまとめようとすると、予期せぬ問題が発生することがあります。この記事では、型宣言ファイルの設計パターンと、発生しがちな問題の解決方法について解説します。

## 問題の背景

以下のような型宣言を一つのファイルに記述したくなるシナリオを考えてみましょう：

```typescript
// 外部ファイルの型宣言
declare module '*.pug'

// グローバル変数やWindowオブジェクトの拡張
declare global {
  const someGlobalVariable: any;
  
  interface Window {
    customProperty?: any;
    analytics?: {
      trackEvent: (event: Event, params: Array<[string, string]>) => void
    }
  }
}

// モジュールとして認識させるための空のエクスポート
export {}
```

このようなファイルを作成すると、一見すべてが機能するように思えますが、以下のような問題が発生することがあります：

1. Pugファイルのインポートができない
2. グローバルWindowオブジェクトの拡張が適用されない
3. どちらかは機能するが、両方は機能しない

## 問題の原因

TypeScriptの型宣言ファイルには、**アンビエント宣言**と**モジュール宣言**の2つの動作モードがあります。

### アンビエント宣言ファイル

`export {}`を含まないファイルは、アンビエント宣言ファイルとして扱われます。このファイル内のすべての宣言はグローバルスコープに適用されます。

```typescript
// アンビエント宣言ファイル
declare module '*.pug' // グローバルに適用される
```

### モジュールファイル

`export {}`を含むファイルはモジュールファイルとして扱われます。このファイル内の宣言はデフォルトでモジュールスコープに閉じ込められます。

```typescript
// モジュールファイル
declare module '*.pug' // モジュールスコープに閉じ込められる可能性がある

declare global {
  // グローバルスコープに宣言を追加するための特別な構文
}

export {} // このファイルをモジュールとして扱うよう指示
```

## 重要なポイント

1. `declare global`ブロックはモジュールファイル内でのみ有効です（`export {}`がある場合）
2. アンビエント宣言ファイルでは`declare global`は不要（すでにグローバルスコープだから）
3. `export {}`を追加すると、ファイル内の他の宣言の適用範囲が変わる可能性があります

## 解決策：宣言ファイルの分離

この問題を解決する最も確実な方法は、宣言ファイルを役割ごとに分離することです：

### 1. モジュール宣言用ファイル

```typescript
// 外部ファイルの型宣言用
declare module '*.pug'
```

このファイルはアンビエント宣言ファイルとして機能し、`export {}`を含みません。

### 2. グローバル宣言用ファイル

```typescript
// グローバル宣言用
declare global {
  const someGlobalVariable: any;
  
  interface Window {
    customProperty?: any;
    analytics?: {
      trackEvent: (event: Event, params: Array<[string, string]>) => void
    }
  }
}

// モジュールとして認識させるための空のエクスポート
export {}
```

このファイルはモジュールファイルとして機能し、`export {}`を含みます。

## 実装のポイント

### 安全なWindowオブジェクト拡張

Windowオブジェクトにカスタムプロパティを追加する場合は、オプショナル（`?`）にすることを推奨します：

```typescript
interface Window {
  analytics?: {
    trackEvent: (event: Event, params: Array<[string, string]>) => void
  }
}
```

そして、実際にアクセスする際は存在チェックを行います：

```typescript
function sendAnalytics(event: Event, name: string): void {
  if (typeof window === 'undefined') return;
  if (typeof window.analytics === 'undefined') return;
  
  window.analytics.trackEvent(event, [['eventName', name]]);
}
```

## TypeScriptの型宣言ファイルに関するドキュメント

TypeScriptの型宣言ファイルについて詳しく知りたい場合は、以下の公式ドキュメントが参考になります：

1. [Declaration Files](https://www.typescriptlang.org/docs/handbook/declaration-files/introduction.html)
2. [Declaration Merging](https://www.typescriptlang.org/docs/handbook/declaration-merging.html)
3. [Modules](https://www.typescriptlang.org/docs/handbook/modules.html)

## まとめ

TypeScriptの型宣言ファイルを設計する際は、以下の点に注意しましょう：

1. **役割ごとにファイルを分離する**：
   - 外部ファイル型宣言用（`.pug`など）
   - グローバル宣言用（`Window`インターフェースなど）

2. **ファイルの種類を意識する**：
   - アンビエント宣言ファイル（`export {}`なし）
   - モジュールファイル（`export {}`あり）

3. **適切な型宣言の場所を選ぶ**：
   - `declare module`はアンビエント宣言ファイルに
   - `declare global`はモジュールファイルに

この設計パターンに従うことで、型宣言ファイルに関する多くの問題を未然に防ぐことができます。

## 参考

- [TypeScript Handbook](https://www.typescriptlang.org/docs/handbook/intro.html)
- [TypeScript Declaration Files](https://www.typescriptlang.org/docs/handbook/declaration-files/introduction.html)
- [Stack Overflow: TypeScript declare global vs export](https://stackoverflow.com/questions/42233987/how-to-configure-custom-global-interfaces-for-typescript)