T-CREATOR

TypeScript namespace と module の正しい使い方と設計指針

TypeScript namespace と module の正しい使い方と設計指針

TypeScript プロジェクトで「namespace と module、どっちを使えばいいの?」と迷った経験はありませんか?コードレビューで「なぜここで namespace を使ったの?」と指摘され、明確に答えられずに困った経験はないでしょうか。さらに、ES Modules という選択肢も加わり、開発現場では混乱が生じているのが現実です。

この記事では、TypeScript のモジュールシステムの正しい使い分けを、現代の開発環境に即した実践的な観点から解説します。歴史的経緯を踏まえつつ、実際のプロジェクトでどのような判断基準で選択すべきか、具体的なコード例とともにお伝えしていきます。

背景:TypeScript モジュールシステムの進化と現在地

TypeScript のモジュールシステムを理解するには、まずその歴史的な経緯を把握することが重要です。なぜなら、現在も使われている複数の手法は、それぞれ異なる時代の要求に応えるために生まれたものだからです。

TypeScript のモジュールシステム進化史

TypeScript が登場した 2012 年当時、JavaScript には標準的なモジュールシステムが存在しませんでした。Node.js では CommonJS、ブラウザ環境では AMD や Script tag による読み込みが主流で、大規模な JavaScript アプリケーション開発は困難を極めていました。

この混沌とした状況の中で、TypeScript は独自のモジュール機能を提供する必要がありました。それがInternal Modules(後のnamespace)とExternal Modules(後のmodule)です。

typescript// 2012年頃のTypeScript(Internal Modules)
module MyLibrary {
  export class Utils {
    static formatDate(date: Date): string {
      return date.toISOString();
    }
  }

  export interface Config {
    apiUrl: string;
    timeout: number;
  }
}

// 使用例
const config: MyLibrary.Config = {
  apiUrl: 'https://api.example.com',
  timeout: 5000,
};

この時代の TypeScript は、C#や Java のような名前空間の概念を持ち込み、JavaScript 開発に秩序をもたらそうとしていました。しかし、2015 年の ES2015(ES6)で ES Modules が標準化されたことで、状況は大きく変わります。

ES Modules 登場前後の変遷

ES Modules の登場は、JavaScript エコシステムに革命をもたらしました。ブラウザと Node.js の両方で統一されたモジュールシステムが使えるようになり、TypeScript もこれに対応する必要がありました。

typescript// ES Modules時代のTypeScript(2015年以降)
// utils.ts
export class Utils {
  static formatDate(date: Date): string {
    return date.toISOString();
  }
}

export interface Config {
  apiUrl: string;
  timeout: number;
}

// main.ts
import { Utils, Config } from './utils';

const config: Config = {
  apiUrl: 'https://api.example.com',
  timeout: 5000,
};

この変化により、TypeScript は既存のnamespace/module機能を維持しながら、ES Modules を完全サポートするという複雑な立場に置かれました。結果として、現在の TypeScript には 3 つの異なるモジュール手法が共存することになったのです。

レガシーコードとの共存問題

現在の開発現場では、異なる時代に書かれたコードが混在しているケースが多く見られます。特に大規模なプロジェクトや長期間運用されているシステムでは、この共存問題が深刻です。

typescript// レガシーなnamespaceベースのコード
declare namespace LegacyAPI {
  interface UserData {
    id: number;
    name: string;
  }

  function getUserById(id: number): Promise<UserData>;
}

// 現代的なES Modulesコード
import { UserService } from './services/user-service';
import { User } from './types/user';

// 両方を使う必要がある場合
class UserController {
  async getUser(id: number): Promise<User> {
    // レガシーAPIとモダンなサービスを組み合わせる
    const legacyData = await LegacyAPI.getUserById(id);
    return this.userService.transformToModernUser(
      legacyData
    );
  }
}

このような状況は、コードの一貫性を損ない、新しい開発者の学習コストを増大させる要因となっています。

課題:現代の開発現場で直面する具体的な問題

TypeScript のモジュールシステムの多様性は、開発現場で様々な課題を生み出しています。ここでは、実際のプロジェクトでよく遭遇する問題を具体的に見ていきましょう。

namespace、module、ES Modules の使い分けの混乱

最も頻繁に遭遇する問題は、「どの手法をいつ使うべきか」という判断の混乱です。チーム内で統一されたルールがない場合、同じプロジェクト内でも異なる手法が混在し、保守性を著しく損ないます。

typescript// 混乱の例:同一プロジェクト内での不統一
// ファイル1:namespace使用
namespace UserModule {
  export interface User {
    id: number;
    name: string;
  }

  export function createUser(name: string): User {
    return { id: Math.random(), name };
  }
}

// ファイル2:ES Modules使用
export interface Product {
  id: number;
  title: string;
}

export function createProduct(title: string): Product {
  return { id: Math.random(), title };
}

// ファイル3:両方を使う羽目になる
import { Product } from './product';
/// <reference path="./user.ts" />

// 使用方法が異なるため、コードの一貫性が失われる
const user = UserModule.createUser('Alice');
const product = createProduct('Laptop');

この不統一は、新しいチームメンバーが参加した際の学習コストを増大させ、コードレビューでの議論を複雑化させます。

グローバルな名前空間汚染問題

namespaceを多用することで発生する典型的な問題が、グローバルな名前空間の汚染です。特に大規模なプロジェクトでは、意図しない名前の衝突が発生しやすくなります。

typescript// 問題のあるnamespace設計
namespace Utils {
  export function formatDate(date: Date): string {
    return date.toLocaleDateString();
  }
}

namespace AdminUtils {
  export function formatDate(date: Date): string {
    return date.toISOString(); // 異なる実装
  }
}

// 別のファイルで
namespace Utils {
  // 意図せずに既存のUtilsを拡張してしまう
  export function formatTime(date: Date): string {
    return date.toLocaleTimeString();
  }
}

// 使用時の混乱
const formatted1 = Utils.formatDate(new Date()); // どちらのformatDateが呼ばれる?
const formatted2 = AdminUtils.formatDate(new Date());

このような問題は、特にマイクロフロントエンドや複数チームでの開発において深刻化します。

バンドラーとの相性問題

現代のフロントエンド開発では、Webpack や Vite などのバンドラーが必須ツールとなっています。しかし、namespaceはこれらのツールとの相性が良くありません。

typescript// namespace使用時の問題
namespace LargeLibrary {
  export class ComponentA {
    render() {
      /* 大量のコード */
    }
  }

  export class ComponentB {
    render() {
      /* 大量のコード */
    }
  }

  export class ComponentC {
    render() {
      /* 大量のコード */
    }
  }
}

// 使用側では1つのクラスしか使わない
const componentA = new LargeLibrary.ComponentA();

この場合、実際にはComponentAしか使用していませんが、バンドラーはLargeLibrary全体を含めてしまい、バンドルサイズが不必要に大きくなってしまいます。

一方、ES Modules を使用すれば、Tree Shaking によって未使用のコードを自動的に除去できます。

typescript// ES Modules使用時の解決例
// components/component-a.ts
export class ComponentA {
  render() {
    /* 大量のコード */
  }
}

// components/component-b.ts
export class ComponentB {
  render() {
    /* 大量のコード */
  }
}

// 使用側
import { ComponentA } from './components/component-a';
// ComponentBは自動的にバンドルから除外される

const componentA = new ComponentA();

解決策:現代的な使い分け原則と実装戦略

これらの課題を解決するために、現代の TypeScript 開発では明確な使い分け原則を確立する必要があります。ここでは、実践的な判断基準と具体的な実装パターンを紹介します。

現代的な使い分け原則(ES Modules 優先の設計指針)

現代の TypeScript 開発では、ES Modules Firstの原則を採用することを強く推奨します。これは、特別な理由がない限り ES Modules を使用し、namespacemoduleは特定の用途に限定するという考え方です。

基本的な選択フローチャート

以下の判断基準に従って、適切なモジュール手法を選択してください。

#条件推奨手法理由
1通常のアプリケーション開発ES ModulesTree Shaking、標準仕様、ツール対応
2型定義のみのライブラリnamespaceグローバル型の提供、宣言マージ
3レガシーコードとの統合namespace既存コードとの互換性維持
4ブラウザ直接読み込みnamespacescript tag での単純な利用
5Node.js 専用パッケージES ModulesCommonJS との相互運用性

ES Modules 優先の具体的実装

typescript// ✅ 推奨:ES Modulesベースの実装
// types/user.ts
export interface User {
  id: number;
  name: string;
  email: string;
}

export interface CreateUserRequest {
  name: string;
  email: string;
}

// services/user-service.ts
import { User, CreateUserRequest } from '../types/user';

export class UserService {
  async createUser(
    request: CreateUserRequest
  ): Promise<User> {
    // 実装
    return {
      id: Math.random(),
      name: request.name,
      email: request.email,
    };
  }

  async getUserById(id: number): Promise<User | null> {
    // 実装
    return null;
  }
}

// controllers/user-controller.ts
import { UserService } from '../services/user-service';
import { CreateUserRequest } from '../types/user';

export class UserController {
  constructor(private userService: UserService) {}

  async handleCreateUser(request: CreateUserRequest) {
    return await this.userService.createUser(request);
  }
}

この実装では、各ファイルが明確な責任を持ち、必要な機能のみをインポートしています。バンドラーによる最適化も効果的に機能します。

namespace 適用パターン(型定義・API ライブラリなど)

namespaceは完全に時代遅れというわけではありません。適切な用途で使用すれば、依然として有効なツールです。

パターン 1:型定義専用ライブラリ

外部ライブラリの型定義や、グローバルに使用される型の定義にはnamespaceが適しています。

typescript// types/api.d.ts
declare namespace API {
  interface BaseResponse {
    success: boolean;
    message?: string;
  }

  namespace User {
    interface GetResponse extends BaseResponse {
      data: {
        id: number;
        name: string;
        email: string;
      };
    }

    interface CreateRequest {
      name: string;
      email: string;
    }

    interface CreateResponse extends BaseResponse {
      data: {
        id: number;
      };
    }
  }

  namespace Product {
    interface GetResponse extends BaseResponse {
      data: {
        id: number;
        title: string;
        price: number;
      };
    }
  }
}

// 使用例
function handleUserResponse(
  response: API.User.GetResponse
) {
  if (response.success) {
    console.log(`User: ${response.data.name}`);
  }
}

このパターンでは、API 関連の型が論理的にグループ化され、使用時に明確な構造を提供します。

パターン 2:ライブラリの内部実装隠蔽

ライブラリを開発する際、内部実装を隠蔽しつつ公開 API を提供する場合にもnamespaceが有効です。

typescript// math-library.ts
export namespace MathUtils {
  // 内部で使用するヘルパー関数(非公開)
  function isValidNumber(value: any): value is number {
    return typeof value === 'number' && !isNaN(value);
  }

  // 公開API
  export function add(a: number, b: number): number {
    if (!isValidNumber(a) || !isValidNumber(b)) {
      throw new Error('Invalid numbers provided');
    }
    return a + b;
  }

  export function multiply(a: number, b: number): number {
    if (!isValidNumber(a) || !isValidNumber(b)) {
      throw new Error('Invalid numbers provided');
    }
    return a * b;
  }

  export const PI = 3.14159;
}

// 使用例
import { MathUtils } from './math-library';

const result = MathUtils.add(1, 2);
const area = MathUtils.PI * Math.pow(radius, 2);

段階的移行戦略とリファクタリング手法

既存のプロジェクトでnamespaceから ES Modules に移行する際は、段階的なアプローチを取ることが重要です。

移行ステップ 1:影響範囲の調査

まず、既存コードでのnamespace使用状況を把握します。

typescript// 移行前の調査例
// 以下のようなコマンドで使用状況を調査
// grep -r "namespace\|module" src/ --include="*.ts"

// 発見された問題のあるパターン
namespace Utils {
  export function formatDate(date: Date): string {
    return date.toLocaleDateString();
  }

  export function formatCurrency(amount: number): string {
    return `$${amount.toFixed(2)}`;
  }
}

// 複数ファイルで使用
const formatted = Utils.formatDate(new Date());

移行ステップ 2:段階的な分割

大きなnamespaceを小さなモジュールに分割します。

typescript// utils/date-formatter.ts
export function formatDate(date: Date): string {
  return date.toLocaleDateString();
}

export function formatDateTime(date: Date): string {
  return `${date.toLocaleDateString()} ${date.toLocaleTimeString()}`;
}

// utils/currency-formatter.ts
export function formatCurrency(
  amount: number,
  currency: string = 'USD'
): string {
  return new Intl.NumberFormat('en-US', {
    style: 'currency',
    currency: currency,
  }).format(amount);
}

// utils/index.ts(barrel export)
export * from './date-formatter';
export * from './currency-formatter';

移行ステップ 3:使用箇所の更新

使用箇所を段階的に更新します。

typescript// 移行前
const formatted = Utils.formatDate(new Date());
const price = Utils.formatCurrency(100);

// 移行後
import { formatDate, formatCurrency } from './utils';

const formatted = formatDate(new Date());
const price = formatCurrency(100);

移行ステップ 4:後方互換性の提供

移行期間中は、後方互換性を提供することで段階的な移行を可能にします。

typescript// utils/legacy-adapter.ts
import * as DateFormatter from './date-formatter';
import * as CurrencyFormatter from './currency-formatter';

// 既存のnamespaceインターフェースを維持
export namespace Utils {
  export const formatDate = DateFormatter.formatDate;
  export const formatCurrency =
    CurrencyFormatter.formatCurrency;
}

// 新しいモジュールも公開
export * from './date-formatter';
export * from './currency-formatter';

具体例:実際のプロジェクトでの適用事例

理論的な説明だけでは実際の適用が困難な場合があります。ここでは、具体的なプロジェクトシナリオを通じて、実践的な適用方法を解説します。

実際のプロジェクトでの適用事例

ケース 1:React アプリケーションでの適用

大規模な React アプリケーションでのモジュール設計例を見てみましょう。

typescript// src/types/api.ts(ES Modules)
export interface ApiResponse<T> {
  data: T;
  success: boolean;
  message?: string;
}

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

export interface Product {
  id: number;
  title: string;
  price: number;
  category: string;
}

// src/services/api-client.ts(ES Modules)
import { ApiResponse, User, Product } from '../types/api';

export class ApiClient {
  private baseUrl: string;

  constructor(baseUrl: string) {
    this.baseUrl = baseUrl;
  }

  async getUser(id: number): Promise<ApiResponse<User>> {
    const response = await fetch(
      `${this.baseUrl}/users/${id}`
    );
    return response.json();
  }

  async getProducts(): Promise<ApiResponse<Product[]>> {
    const response = await fetch(
      `${this.baseUrl}/products`
    );
    return response.json();
  }
}

// src/hooks/use-api.ts(ES Modules)
import { useState, useEffect } from 'react';
import { ApiClient } from '../services/api-client';
import { ApiResponse } from '../types/api';

export function useApi<T>(
  apiCall: () => Promise<ApiResponse<T>>
) {
  const [data, setData] = useState<T | null>(null);
  const [loading, setLoading] = useState(false);
  const [error, setError] = useState<string | null>(null);

  useEffect(() => {
    const fetchData = async () => {
      setLoading(true);
      try {
        const response = await apiCall();
        if (response.success) {
          setData(response.data);
        } else {
          setError(response.message || 'Unknown error');
        }
      } catch (err) {
        setError(
          err instanceof Error
            ? err.message
            : 'Unknown error'
        );
      } finally {
        setLoading(false);
      }
    };

    fetchData();
  }, []);

  return { data, loading, error };
}

// src/components/UserProfile.tsx(ES Modules)
import React from 'react';
import { useApi } from '../hooks/use-api';
import { ApiClient } from '../services/api-client';
import { User } from '../types/api';

interface UserProfileProps {
  userId: number;
  apiClient: ApiClient;
}

export const UserProfile: React.FC<UserProfileProps> = ({
  userId,
  apiClient,
}) => {
  const {
    data: user,
    loading,
    error,
  } = useApi<User>(() => apiClient.getUser(userId));

  if (loading) return <div>Loading...</div>;
  if (error) return <div>Error: {error}</div>;
  if (!user) return <div>User not found</div>;

  return (
    <div>
      <h2>{user.name}</h2>
      <p>Email: {user.email}</p>
      <p>Role: {user.role}</p>
    </div>
  );
};

この React プロジェクトでは、ES Modules を一貫して使用することで以下のメリットを得られます:

  • Tree Shaking: 未使用のコンポーネントやユーティリティが自動的にバンドルから除外される
  • 明確な依存関係: 各ファイルが何に依存しているかが明確
  • 再利用性: 各モジュールが独立しており、他のプロジェクトでも再利用可能

ケース 2:Node.js バックエンドでの適用

Express.js を使用したバックエンドアプリケーションでの実装例です。

typescript// src/types/database.ts(ES Modules)
export interface DatabaseUser {
  id: number;
  username: string;
  email: string;
  password_hash: string;
  created_at: Date;
  updated_at: Date;
}

export interface CreateUserData {
  username: string;
  email: string;
  password: string;
}

// src/repositories/user-repository.ts(ES Modules)
import {
  DatabaseUser,
  CreateUserData,
} from '../types/database';

export class UserRepository {
  constructor(private database: any) {} // 実際のDB接続オブジェクト

  async findById(id: number): Promise<DatabaseUser | null> {
    const result = await this.database.query(
      'SELECT * FROM users WHERE id = ?',
      [id]
    );
    return result[0] || null;
  }

  async create(
    userData: CreateUserData
  ): Promise<DatabaseUser> {
    const result = await this.database.query(
      'INSERT INTO users (username, email, password_hash) VALUES (?, ?, ?)',
      [userData.username, userData.email, userData.password]
    );
    return this.findById(result.insertId);
  }
}

// src/services/user-service.ts(ES Modules)
import { UserRepository } from '../repositories/user-repository';
import {
  CreateUserData,
  DatabaseUser,
} from '../types/database';
import bcrypt from 'bcrypt';

export class UserService {
  constructor(private userRepository: UserRepository) {}

  async createUser(
    userData: CreateUserData
  ): Promise<DatabaseUser> {
    // パスワードのハッシュ化
    const hashedPassword = await bcrypt.hash(
      userData.password,
      10
    );

    const userToCreate = {
      ...userData,
      password: hashedPassword,
    };

    return await this.userRepository.create(userToCreate);
  }

  async getUserById(
    id: number
  ): Promise<DatabaseUser | null> {
    return await this.userRepository.findById(id);
  }
}

// src/controllers/user-controller.ts(ES Modules)
import { Request, Response } from 'express';
import { UserService } from '../services/user-service';

export class UserController {
  constructor(private userService: UserService) {}

  async createUser(
    req: Request,
    res: Response
  ): Promise<void> {
    try {
      const userData = req.body;
      const user = await this.userService.createUser(
        userData
      );
      res.status(201).json({ success: true, data: user });
    } catch (error) {
      res.status(400).json({
        success: false,
        message:
          error instanceof Error
            ? error.message
            : 'Unknown error',
      });
    }
  }

  async getUser(
    req: Request,
    res: Response
  ): Promise<void> {
    try {
      const userId = parseInt(req.params.id);
      const user = await this.userService.getUserById(
        userId
      );

      if (!user) {
        res
          .status(404)
          .json({
            success: false,
            message: 'User not found',
          });
        return;
      }

      res.json({ success: true, data: user });
    } catch (error) {
      res.status(500).json({
        success: false,
        message:
          error instanceof Error
            ? error.message
            : 'Unknown error',
      });
    }
  }
}

ケース 3:型定義ライブラリでの namespace 活用

外部 API の型定義など、グローバルに参照される型定義にはnamespaceが適しています。

typescript// types/stripe-api.d.ts(namespace使用)
declare namespace Stripe {
  interface Customer {
    id: string;
    email: string;
    name?: string;
    created: number;
    metadata: Record<string, string>;
  }

  interface PaymentIntent {
    id: string;
    amount: number;
    currency: string;
    status:
      | 'requires_payment_method'
      | 'requires_confirmation'
      | 'succeeded'
      | 'canceled';
    customer?: string;
  }

  namespace API {
    interface CreateCustomerRequest {
      email: string;
      name?: string;
      metadata?: Record<string, string>;
    }

    interface CreatePaymentIntentRequest {
      amount: number;
      currency: string;
      customer?: string;
      metadata?: Record<string, string>;
    }

    interface ListResponse<T> {
      object: 'list';
      data: T[];
      has_more: boolean;
      url: string;
    }
  }
}

// services/stripe-service.ts(ES Modules + namespace型使用)
export class StripeService {
  private stripe: any; // Stripe SDK

  constructor(apiKey: string) {
    // Stripe SDKの初期化
  }

  async createCustomer(
    request: Stripe.API.CreateCustomerRequest
  ): Promise<Stripe.Customer> {
    const customer = await this.stripe.customers.create(
      request
    );
    return customer;
  }

  async createPaymentIntent(
    request: Stripe.API.CreatePaymentIntentRequest
  ): Promise<Stripe.PaymentIntent> {
    const paymentIntent =
      await this.stripe.paymentIntents.create(request);
    return paymentIntent;
  }

  async listCustomers(): Promise<
    Stripe.API.ListResponse<Stripe.Customer>
  > {
    const customers = await this.stripe.customers.list();
    return customers;
  }
}

TypeScript 設定とビルド環境での最適化

適切なモジュール設計を実現するためには、TypeScript の設定とビルド環境の最適化も重要です。

tsconfig.json の最適化

json{
  "compilerOptions": {
    // モジュール関連の設定
    "module": "ES2020",
    "moduleResolution": "node",
    "target": "ES2020",

    // ES Modules優先の設定
    "esModuleInterop": true,
    "allowSyntheticDefaultImports": true,

    // 厳密な型チェック
    "strict": true,
    "noImplicitAny": true,
    "noImplicitReturns": true,
    "noImplicitThis": true,

    // 未使用コードの検出
    "noUnusedLocals": true,
    "noUnusedParameters": true,

    // パス解決の最適化
    "baseUrl": "./src",
    "paths": {
      "@/*": ["*"],
      "@/types/*": ["types/*"],
      "@/services/*": ["services/*"],
      "@/utils/*": ["utils/*"]
    },

    // 出力設定
    "outDir": "./dist",
    "declaration": true,
    "declarationMap": true,
    "sourceMap": true
  },
  "include": ["src/**/*"],
  "exclude": [
    "node_modules",
    "dist",
    "**/*.test.ts",
    "**/*.spec.ts"
  ]
}

Webpack 設定の最適化

javascript// webpack.config.js
const path = require('path');

module.exports = {
  mode: 'production',
  entry: './src/index.ts',

  module: {
    rules: [
      {
        test: /\.tsx?$/,
        use: 'ts-loader',
        exclude: /node_modules/,
      },
    ],
  },

  resolve: {
    extensions: ['.tsx', '.ts', '.js'],
    alias: {
      '@': path.resolve(__dirname, 'src'),
    },
  },

  // Tree Shakingの最適化
  optimization: {
    usedExports: true,
    sideEffects: false,

    splitChunks: {
      chunks: 'all',
      cacheGroups: {
        vendor: {
          test: /[\\/]node_modules[\\/]/,
          name: 'vendors',
          chunks: 'all',
        },
      },
    },
  },

  output: {
    filename: '[name].[contenthash].js',
    path: path.resolve(__dirname, 'dist'),
    clean: true,
  },
};

まとめ:選択基準の明確化とベストプラクティス

TypeScript のモジュールシステムを適切に活用するためには、明確な選択基準とベストプラクティスの確立が不可欠です。本記事で解説した内容を基に、実践的なガイドラインをまとめます。

選択基準の明確化

基本原則:ES Modules First

現代の TypeScript 開発では、ES Modules Firstの原則を採用し、以下の判断基準に従って選択してください:

  1. デフォルト選択: ES Modules (import/export)
  2. 特殊用途のみ: namespaceを使用
  3. 非推奨: moduleキーワード(歴史的理由のみで使用)

具体的な選択フローチャート

typescript// Step 1: 用途の確認
if (アプリケーション開発 || ライブラリ開発) {
  // ES Modulesを使用
  export class MyClass {}
  export interface MyInterface {}
  export function myFunction() {}
} else if (型定義のみの提供 || グローバル型の定義) {
  // namespaceを使用
  declare namespace MyAPI {
    interface Response {}
    interface Request {}
  }
} else if (レガシーコードとの互換性が必要) {
  // 段階的移行戦略を採用
  // まずはnamespaceで互換性を保ちつつ、徐々にES Modulesに移行
}

ベストプラクティス

1. プロジェクト設計のベストプラクティス

typescript// ✅ 推奨:機能別のモジュール分割
// user/types.ts
export interface User {
  id: number;
  name: string;
  email: string;
}

// user/service.ts
import { User } from './types';
export class UserService {
  async getUser(id: number): Promise<User> {
    /* 実装 */
  }
}

// user/index.ts (barrel export)
export * from './types';
export * from './service';

// ❌ 非推奨:namespaceによる巨大なモジュール
namespace UserModule {
  export interface User {
    /* ... */
  }
  export class UserService {
    /* ... */
  }
  export class UserRepository {
    /* ... */
  }
  export class UserController {
    /* ... */
  }
}

2. 型定義のベストプラクティス

typescript// ✅ 推奨:外部API型定義でのnamespace活用
declare namespace ExternalAPI {
  interface BaseResponse {
    success: boolean;
    message?: string;
  }

  namespace Users {
    interface GetResponse extends BaseResponse {
      data: User;
    }
  }
}

// ✅ 推奨:内部型定義でのES Modules
// types/user.ts
export interface User {
  id: number;
  name: string;
}

export interface CreateUserRequest {
  name: string;
}

3. 移行戦略のベストプラクティス

typescript// Step 1: 影響範囲を特定
// - namespace使用箇所の洗い出し
// - 依存関係の整理
// - 移行優先度の決定

// Step 2: 段階的な移行
// 旧コード(namespace)
namespace Utils {
  export function formatDate(date: Date): string {
    return date.toLocaleDateString();
  }
}

// 移行中(両方をサポート)
// utils/date-formatter.ts
export function formatDate(date: Date): string {
  return date.toLocaleDateString();
}

// utils/legacy.ts
import { formatDate as _formatDate } from './date-formatter';
export namespace Utils {
  export const formatDate = _formatDate;
}

// Step 3: 完全移行
// utils/index.ts
export { formatDate } from './date-formatter';

4. ツール設定のベストプラクティス

json// tsconfig.json
{
  "compilerOptions": {
    "module": "ES2020",
    "moduleResolution": "node",
    "esModuleInterop": true,
    "allowSyntheticDefaultImports": true,
    "strict": true,
    "noUnusedLocals": true,
    "noUnusedParameters": true
  }
}
javascript// eslint設定例
module.exports = {
  rules: {
    // namespaceの使用を制限
    '@typescript-eslint/no-namespace': [
      'error',
      {
        allowDeclarations: true, // declare namespaceは許可
        allowDefinitionFiles: true, // .d.tsファイルでは許可
      },
    ],

    // 未使用importの検出
    '@typescript-eslint/no-unused-vars': 'error',

    // 一貫したimport順序
    'import/order': [
      'error',
      {
        groups: [
          'builtin',
          'external',
          'internal',
          'parent',
          'sibling',
          'index',
        ],
      },
    ],
  },
};

最終的な推奨事項

現代の TypeScript 開発において、以下のアプローチを推奨します:

  1. 新規プロジェクト: ES Modules を一貫して使用
  2. 既存プロジェクト: 段階的に ES Modules に移行
  3. 型定義ライブラリ: 必要に応じてnamespaceを活用
  4. チーム開発: 明確なルールを設定し、lint 設定で強制
  5. 継続的改善: 定期的にモジュール設計を見直し、最適化

TypeScript のモジュールシステムは、適切に活用することで、保守性が高く、スケーラブルなアプリケーションの構築を可能にします。本記事で紹介した原則とベストプラクティスを参考に、プロジェクトに最適なモジュール設計を実現していただければと思います。

関連リンク