TypeScript 速習チュートリアル

TypeScript ネームスペース

1. TypeScript Namespace の理解

TypeScript の Namespace(ネームスペース/名前空間)(旧称:内部モジュール)は、関連する機能を一つのコンテナにまとめ、名前の衝突を防ぎながらコードを整理するための強力な手法です。
大規模なコードベースを構造化し、クリーンでメンテナンス性の高い方法でスコープを管理するのに役立ちます。

1.1 主要なコンセプト

  • 論理的なグループ化: 関連するコードを名前付きコンテナに整理します。
  • スコープ管理: コード要素の可視性を制御します。
  • 名前衝突の防止: 似た名前を持つコンポーネント間の競合を回避します。
  • コードの組織化: 大規模なアプリケーションを階層的な構造で構築します。

1.2 Namespace を使用すべき場面

  • 大規模なレガシーアプリケーションのコード整理
  • グローバルライブラリの操作
  • 古い JavaScript コードベースからの移行時
  • グローバルに利用可能である必要があるコードを扱う場合

       注意: TypeScript では Namespace が引き続き完全にサポートされていますが、モダンなアプリケーションでは、より優れたモジュール性とツリーシェイキング(Tree-shaking)をサポートする ES Modules (import/export) を使用するのが一般的です。しかし、レガシーコードの保守や特定のライブラリ開発シナリオにおいて、Namespace の理解は非常に価値があります。

2. 基本的な Namespace の構文

Namespace は namespace キーワードを使用して定義します。

コード例

namespace Validation {
  // このブロック内のすべては Validation ネームスペースに属します

  // ネームスペースの外で利用可能にしたいものには export を付与します
  export interface StringValidator {
    isValid(s: string): boolean;
  }

  // これはネームスペース内でのみ有効(export されていないためプライベート)
  const lettersRegexp = /^[A-Za-z]+$/;

  // export されたクラス - ネームスペース外で使用可能
  export class LettersValidator implements StringValidator {
    isValid(s: string): boolean {
      return lettersRegexp.test(s);
    }
  }

  // 別の export されたクラス
  export class ZipCodeValidator implements StringValidator {
    isValid(s: string): boolean {
      return /^[0-9]+$/.test(s) && s.length === 5;
    }
  }
}

// ネームスペースのメンバーを使用する
let letterValidator = new Validation.LettersValidator();
let zipCodeValidator = new Validation.ZipCodeValidator();

console.log(letterValidator.isValid("Hello")); // true
console.log(letterValidator.isValid("Hello123")); // false

console.log(zipCodeValidator.isValid("12345")); // true
console.log(zipCodeValidator.isValid("1234")); // false - 長さが正しくない

3. 高度な Namespace の機能

3.1 ネストされた Namespace

Namespace はネスト(入れ子)にして、階層的な組織構造を作ることができます。

コード例

namespace App {
  export namespace Utils {
    export function log(msg: string): void {
      console.log(`[LOG]: ${msg}`);
    }

    export function error(msg: string): void {
      console.error(`[ERROR]: ${msg}`);
    }
  }

  export namespace Models {
    export interface User {
      id: number;
      name: string;
      email: string;
    }

    export class UserService {
      getUser(id: number): User {
        return { id, name: "John Doe", email: "[email protected]" };
      }
    }
  }
}

// ネストされたネームスペースの使用
App.Utils.log("アプリケーションを起動中");

const userService = new App.Models.UserService();
const user = userService.getUser(1);

App.Utils.log(`ユーザーがロードされました: ${user.name}`);

// これは TypeScript で型エラーになります
// App.log("直接 log にアクセス"); // エラー - log は App の直接のメンバーではありません

3.2 Namespace のエイリアス

長い名前を扱いやすくするために、ネームスペースやそのメンバーに対してエイリアス(別名)を作成できます。

コード例

namespace VeryLongNamespace {
  export namespace DeeplyNested {
    export namespace Components {
      export class Button {
        display(): void {
          console.log("ボタンが表示されました");
        }
      }
      export class TextField {
        display(): void {
          console.log("テキストフィールドが表示されました");
        }
      }
    }
  }
}

// エイリアスなし - 非常に冗長
const button1 = new VeryLongNamespace.DeeplyNested.Components.Button();
button1.display();

// ネームスペースエイリアスを使用
import Components = VeryLongNamespace.DeeplyNested.Components;
const button2 = new Components.Button();
button2.display();

// 特定のメンバーにエイリアスを使用
import Button = VeryLongNamespace.DeeplyNested.Components.Button;
const button3 = new Button();
button3.display();

4. 複数ファイルにまたがる Namespace

大規模なアプリケーションでは、コードを複数のファイルに分割する必要があります。Namespace はファイルをまたいで定義でき、コンパイル時に結合することが可能です。

4.1 リファレンスコメントの使用

/// <reference path="..." /> コメントは、TypeScript がファイル間の関係を理解するのに役立ちます。

validators.ts

namespace Validation {
  export interface StringValidator {
    isValid(s: string): boolean;
  }
}

letters-validator.ts(Validation ネームスペースを拡張)

/// <reference path="validators.ts" />
namespace Validation {
  const lettersRegexp = /^[A-Za-z]+$/;

  export class LettersValidator implements StringValidator {
    isValid(s: string): boolean {
      return lettersRegexp.test(s);
    }
  }
}

zipcode-validator.ts

/// <reference path="validators.ts" />
namespace Validation {
  const zipCodeRegexp = /^[0-9]+$/;

  export class ZipCodeValidator implements StringValidator {
    isValid(s: string): boolean {
      return zipCodeRegexp.test(s) && s.length === 5;
    }
  }
}

main.ts

/// <reference path="validators.ts" />
/// <reference path="letters-validator.ts" />
/// <reference path="zipcode-validator.ts" />

let validators: { [s: string]: Validation.StringValidator } = {};
validators["letters"] = new Validation.LettersValidator();
validators["zipcode"] = new Validation.ZipCodeValidator();

let strings = ["Hello", "98052", "101"];

strings.forEach(s => {
  for (let name in validators) {
    console.log(`"${s}" - ${validators[name].isValid(s) ? "一致" : "不一致"} (${name})`);
  }
});

これらのファイルを一つの JavaScript ファイルにコンパイルするには、以下のコマンドを使用します:
tsc --outFile sample.js main.ts

5. Namespace vs. Modules

TypeScript 開発において、Namespace と Module(モジュール)のどちらを使用すべきかを知ることは非常に重要です。

  • Modules: モダンな TypeScript アプリケーションでコードを整理するための推奨される方法です。
  • Namespaces: 宣言の併合(Declaration Merging)や、レガシーコードを扱うなどの特定のシナリオで依然として有用です。

5.1 比較表

機能NamespacesES Modules (import/export)
推奨される規模シンプルな設定、小規模アプリ、レガシーコードあらゆる規模のモダンアプリ(新規推奨)
構文と使用法ドット記法によるグローバルアクセスファイルパスによる明示的な import/export
ロード/バンドルローダー不要。--outFile で単一出力可Webpack や Vite 等のバンドラーが必要
ファイル分割/// <reference /> コメントを使用自然な分割。各ファイルが明示的モジュール
Tree-shaking限定的。未使用コードの削除が困難優秀。デッドコード削除を前提に設計
グローバルスコープグローバルを推奨(ネームスペース化される)グローバルを回避し、明示的に依存を定義
拡張/併合宣言の併合により強力にサポートモジュール拡張は可能だが制約がある
エコシステムモダンツールチェーンとの親和性は低い現代のツールやプラットフォームで最適

6. 高度な Namespace パターン

Declaration Merging(宣言の併合) を使用して、既存のライブラリ(例:Express)の型を拡張する例です。

コード例

// オリジナルのネームスペース定義
declare namespace Express {
  interface Request {
    user?: { id: number; name: string };
  }
  interface Response {
    json(data: any): void;
  }
}

// アプリケーションの別の場所(例: .d.ts ファイル)で拡張
declare namespace Express {
  // Request インターフェースを拡張
  interface Request {
    // カスタムプロパティを追加
    requestTime?: number;
    // メソッドを追加
    log(message: string): void;
  }

  // 新しい型を追加
  interface UserSession {
    userId: number;
    expires: Date;
  }
}

// 使用例
const app = express();

app.use((req: Express.Request, res: Express.Response, next) => {
  // 拡張されたプロパティとメソッドが利用可能
  req.requestTime = Date.now();
  req.log('リクエストが開始されました');
  next();
});

7. ジェネリクスを用いた Namespace

コード例

// ジェネリクスを用いたネームスペースの例
namespace DataStorage {
  export interface Repository<T> {
    getAll(): T[];
    getById(id: number): T | undefined;
    add(item: T): void;
    update(id: number, item: T): boolean;
    delete(id: number): boolean;
  }

  // 具体的な実装
  export class InMemoryRepository<T> implements Repository<T> {
    private items: T[] = [];

    getAll(): T[] {
      return [...this.items];
    }

    getById(id: number): T | undefined {
      return this.items[id];
    }

    add(item: T): void {
      this.items.push(item);
    }

    update(id: number, item: T): boolean {
      if (id >= 0 && id < this.items.length) {
        this.items[id] = item;
        return true;
      }
      return false;
    }

    delete(id: number): boolean {
      if (id >= 0 && id < this.items.length) {
        this.items.splice(id, 1);
        return true;
      }
      return false;
    }
  }
}

// 使用例
interface User {
  id: number;
  name: string;
  email: string;
}

const userRepo = new DataStorage.InMemoryRepository<User>();
userRepo.add({ id: 1, name: 'John Doe', email: '[email protected]' });
const allUsers = userRepo.getAll();

8. ベストプラクティス

8.1 Namespace のベストプラクティス

  • Do: 意味のある階層的なネームスペース名を使用する。
  • Do: 必要なものだけを export する。
  • Do: 複数ファイルの場合は /// <reference /> で順序を管理する。
  • Do: 新規プロジェクトではモジュールの使用を優先的に検討する。
  • Do: パフォーマンス向上のため、ネームスペース内で const enum を活用する。
  • Do: JSDoc コメントでドキュメント化する。

8.2 パフォーマンスに関する考慮事項

  • 巨大な Namespace はバンドルサイズを増大させる可能性があります。
  • 大規模なアプリではコード分割(Code Splitting)を検討してください。
  • 複雑なネームスペース構造における循環参照に注意してください。
  • 定数には const enum を使用して、インライン展開による最適化を図りましょう。

9. Namespace から Module への移行

コード例

// Before: Namespace を使用
namespace MyApp {
  export namespace Services {
    export class UserService {
      getUser(id: number) { /* ... */ }
    }
  }
}

// After: ES Modules を使用
// services/UserService.ts
export class UserService {
  getUser(id: number) { /* ... */ }
}

// app.ts
import { UserService } from './services/UserService';
const userService = new UserService();

9.1 移行のステップ

  1. 各 Namespace を個別のモジュールファイルに変換する。
  2. export を ES モジュールの export に置き換える。
  3. インポートを ES モジュールの構文(import { ... })に更新する。
  4. ビルドシステム(Vite, Webpack 等)をモジュール対応に設定する。
  5. tsconfig.json"module" 設定を "ESNext" などに更新する。

9.2 移行ツール

  • ts-migrate: Facebook が提供する、Namespace からモジュールへの移行を自動化するツール。
  • ESLint: no-namespace ルールを使用して、新規の Namespace 作成を制限。