T-CREATOR

<div />

TypeScriptのnamespaceとmoduleを設計で使い分ける 正しい使い方と判断指針

2026年1月17日
TypeScriptのnamespaceとmoduleを設計で使い分ける 正しい使い方と判断指針

TypeScript プロジェクトで「namespace と ES Modules、どちらを使うべきか」という判断に迷った経験はないでしょうか。実務で namespace と ES Modules を併用した結果、ビルド時の Tree Shaking が効かずバンドルサイズが肥大化したり、型定義ファイルとの整合性が崩れたりする問題に直面しました。本記事では、併用の落とし穴を整理し、tsconfig.json の設定を含めた現代のモジュール運用で破綻しない選び方を、型安全な設計の観点から解説します。

namespace と ES Modules の比較

観点namespaceES Modules
主な用途型定義・レガシー対応アプリケーション開発全般
Tree Shaking非対応対応
静的型付けとの相性宣言マージで拡張可能型推論が自然に効く
バンドラー対応限定的完全対応
tsconfig.json 設定特別な設定不要module オプション必須
型安全の担保手動管理が必要import で自動追跡
推奨度(2026年現在)限定的な用途のみ標準として推奨

それぞれの詳細は後述します。

検証環境

  • OS: macOS Sonoma 15.2
  • Node.js: 22.13.0
  • TypeScript: 5.7.3
  • 主要パッケージ:
    • vite: 6.0.7
    • @typescript-eslint/eslint-plugin: 8.20.0
  • 検証日: 2026 年 01 月 17 日

TypeScript モジュールシステムの歴史的背景

この章では、なぜ TypeScript に複数のモジュール手法が存在するのかを解説します。

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

つまずきやすい点:namespace(旧 Internal Modules)と module キーワードは、歴史的には同じ機能を別名で呼んでいたものです。現在 module キーワードは非推奨です。

TypeScript はこの状況に対応するため、独自のモジュール機能を提供しました。それが namespace(当時は Internal Modules)です。

typescript// 2012年頃のTypeScript設計パターン
namespace MyLibrary {
  export class Utils {
    static formatDate(date: Date): string {
      return date.toISOString();
    }
  }

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

// 使用例:namespace経由でアクセス
const config: MyLibrary.Config = {
  apiUrl: "https://api.example.com",
  timeout: 5000,
};

2015 年の ES2015 で ES Modules が標準化されたことで、状況は大きく変わりました。ブラウザと 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";

この結果、現在の TypeScript には namespace と ES Modules という2つの手法が共存しています。

namespace と ES Modules が混在する実務上の問題

この章では、現場で実際に発生する問題を整理します。

チーム内での設計指針の不統一

最も頻繁に遭遇する問題は、「どの手法をいつ使うべきか」という判断の混乱です。実際に試したところ、同じプロジェクト内で namespace と ES Modules が混在すると、以下のような状況が発生しました。

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

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

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

const user = UserModule.createUser("Alice"); // namespace経由
const product = createProduct("Laptop"); // 直接呼び出し

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

Tree Shaking が効かないバンドルサイズの肥大化

業務で問題になった典型例がバンドルサイズです。namespace を多用すると、バンドラーの最適化が効きません。

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

// 使用側では1つのクラスしか使わない
const componentA = new LargeLibrary.ComponentA();
// → ComponentB, ComponentC もバンドルに含まれてしまう

検証の結果、ES Modules に書き換えることで未使用コードが除外され、バンドルサイズが約 40% 削減できたケースがありました。

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

// 使用側
import { ComponentA } from "./components/component-a";
// → ComponentB, ComponentC は自動的にバンドルから除外

グローバル名前空間の汚染

namespace を複数ファイルで使用すると、意図しない拡張や衝突が発生します。

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

// file2.ts(別の開発者が追加)
namespace Utils {
  // 意図せず既存のUtilsを拡張してしまう
  export function formatTime(date: Date): string {
    return date.toLocaleTimeString();
  }
}

この「宣言マージ」機能は型定義では便利ですが、アプリケーションコードでは予期しない挙動の原因になります。

モジュール選択の判断フロー

この章では、どのような状況でどちらを選ぶべきかを図解します。

以下のフローチャートに従って判断することで、設計の一貫性を保てます。

mermaidflowchart TD
  Start["モジュール手法の<br/>選択開始"] --> Q1{"アプリケーション<br/>コードか?"}
  Q1 -->|Yes| ESM["ES Modules を使用"]
  Q1 -->|No| Q2{"型定義のみ<br/>提供するか?"}
  Q2 -->|Yes| NS["namespace を使用"]
  Q2 -->|No| Q3{"レガシーコード<br/>との互換性が<br/>必要か?"}
  Q3 -->|Yes| MIG["段階的移行戦略"]
  Q3 -->|No| ESM
  MIG --> NS2["一時的に namespace<br/>で互換性維持"]
  NS2 --> ESM2["最終的に<br/>ES Modules へ"]

この図は「アプリケーションコードなら ES Modules、型定義のみなら namespace」という基本原則を示しています。

ES Modules 優先の設計指針と tsconfig.json 設定

この章では、推奨される設計パターンと必要な設定を解説します。

基本原則:ES Modules First

現代の TypeScript 開発では、ES Modules First の原則を採用します。特別な理由がない限り ES Modules を使用し、namespace は型定義など特定の用途に限定します。

条件推奨手法理由
通常のアプリケーション開発ES ModulesTree Shaking・標準仕様・ツール対応
型定義のみのライブラリnamespaceグローバル型の提供・宣言マージ
レガシーコードとの統合namespace(一時的)既存コードとの互換性維持
外部 API の型定義namespace論理的なグループ化

tsconfig.json の推奨設定

静的型付けと型安全を最大限に活かすため、以下の設定を推奨します。

json{
  "compilerOptions": {
    "module": "ES2022",
    "moduleResolution": "bundler",
    "target": "ES2022",

    "esModuleInterop": true,
    "allowSyntheticDefaultImports": true,

    "strict": true,
    "noImplicitAny": true,
    "strictNullChecks": true,

    "noUnusedLocals": true,
    "noUnusedParameters": true,

    "baseUrl": "./src",
    "paths": {
      "@/*": ["*"],
      "@/types/*": ["types/*"],
      "@/services/*": ["services/*"]
    },

    "declaration": true,
    "declarationMap": true,
    "sourceMap": true
  },
  "include": ["src/**/*"],
  "exclude": ["node_modules", "dist"]
}

つまずきやすい点moduleResolution: "bundler" は TypeScript 5.0 以降で追加された設定です。Vite や webpack などのバンドラーを使用する場合に最適化されています。

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,
    };
  }
}

// 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);
  }
}

この設計では、import 文により依存関係が明示され、TypeScript の型推論が自然に効きます。

namespace が適切な用途と実装パターン

この章では、namespace を使うべき限定的なケースを解説します。

パターン1:外部 API の型定義

外部ライブラリやサードパーティ API の型定義には namespace が適しています。

typescript// types/external-api.d.ts
declare namespace ExternalAPI {
  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;
    }
  }

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

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

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

パターン2:グローバルに参照される型の拡張

既存のライブラリの型を拡張する場合にも namespace の宣言マージが有効です。

typescript// types/express-extensions.d.ts
declare namespace Express {
  interface Request {
    user?: {
      id: number;
      role: "admin" | "user";
    };
  }
}

採用しなかった設計:namespace によるアプリケーション構造化

検証中に起きた失敗として、namespace をアプリケーションの構造化に使おうとしたケースがあります。

typescript// ❌ 採用しなかった設計
namespace Application {
  export namespace Services {
    export class UserService {
      /* 実装 */
    }
  }
  export namespace Controllers {
    export class UserController {
      /* 実装 */
    }
  }
}

この設計は以下の理由で採用しませんでした:

  • Tree Shaking が効かない
  • ファイル分割が困難
  • テストのモック化が複雑になる
  • IDE の自動インポートが正しく機能しない

namespace から ES Modules への段階的移行戦略

この章では、既存プロジェクトの移行手順を解説します。

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

まず、namespace の使用状況を把握します。

bash# namespace使用箇所の洗い出し
grep -r "namespace\|module" src/ --include="*.ts"

ステップ2:新規 ES Modules の作成

既存の namespace を維持しつつ、ES Modules 版を並行して作成します。

typescript// ❌ 移行前:namespace
namespace Utils {
  export function formatDate(date: Date): string {
    return date.toLocaleDateString();
  }
  export function formatCurrency(amount: number): string {
    return `$${amount.toFixed(2)}`;
  }
}

// ✅ 移行後:ES Modules
// utils/date-formatter.ts
export function formatDate(date: Date): string {
  return date.toLocaleDateString();
}

// 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// utils/legacy-adapter.ts
import * as DateFormatter from "./date-formatter";
import * as CurrencyFormatter from "./currency-formatter";

/** @deprecated ES Modulesからの直接importを推奨 */
export namespace Utils {
  export const formatDate = DateFormatter.formatDate;
  export const formatCurrency = CurrencyFormatter.formatCurrency;
}

export * from "./date-formatter";
export * from "./currency-formatter";

ステップ4:使用箇所の更新と旧コード削除

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

// 移行後
import { formatDate } from "./utils";
const formatted = formatDate(new Date());

実務での適用事例

この章では、具体的なプロジェクトでの実装例を紹介します。

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";
}

// src/services/api-client.ts(ES Modules)
import { ApiResponse, User } 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();
  }
}

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

export function useUser(apiClient: ApiClient, userId: number) {
  const [user, setUser] = useState<User | null>(null);
  const [loading, setLoading] = useState(true);
  const [error, setError] = useState<string | null>(null);

  useEffect(() => {
    const fetchUser = async () => {
      try {
        const response = await apiClient.getUser(userId);
        if (response.success) {
          setUser(response.data);
        } else {
          setError(response.message || "Unknown error");
        }
      } catch (err) {
        setError(err instanceof Error ? err.message : "Unknown error");
      } finally {
        setLoading(false);
      }
    };
    fetchUser();
  }, [apiClient, userId]);

  return { user, loading, error };
}

ESLint による設計ルールの強制

tsconfig.json と組み合わせて、チーム全体で設計指針を統一します。

javascript// eslint.config.js(Flat Config形式)
import tseslint from "@typescript-eslint/eslint-plugin";

export default [
  {
    plugins: {
      "@typescript-eslint": tseslint,
    },
    rules: {
      // namespaceの使用を制限(型定義ファイルは許可)
      "@typescript-eslint/no-namespace": [
        "error",
        {
          allowDeclarations: true,
          allowDefinitionFiles: true,
        },
      ],
      "@typescript-eslint/no-unused-vars": "error",
    },
  },
];

namespace と ES Modules の比較まとめ

観点namespaceES Modules
推奨用途型定義ファイル(.d.ts)外部 API の型定義レガシー互換レイヤーアプリケーション開発ライブラリ開発新規プロジェクト全般
Tree Shaking❌ 非対応✅ 完全対応
静的型付け宣言マージで拡張可能import による型追跡
バンドラー対応限定的Vite・webpack 完全対応
tsconfig.json特別な設定不要modulemoduleResolution 設定必須
IDE 支援自動インポートが不安定自動インポート完全対応
テスタビリティモック化が困難依存性注入が容易
将来性メンテナンスモードECMAScript 標準として発展中

向いているケース

namespace が向いているケース

  • サードパーティライブラリの型定義(DefinitelyTyped)
  • グローバルに参照される型の論理的グループ化
  • 既存の namespace ベースコードとの互換性維持

ES Modules が向いているケース

  • 新規のアプリケーション開発
  • バンドルサイズの最適化が重要なプロジェクト
  • チーム開発で一貫性を重視する場合

まとめ

TypeScript の namespace と ES Modules は、それぞれ異なる時代の要求に応えて生まれた機能です。現代の開発では ES Modules First の原則を採用し、namespace は型定義など限定的な用途に絞ることで、型安全で保守性の高いコードベースを実現できます。

ただし、レガシーコードを抱えるプロジェクトでは段階的な移行が現実的です。後方互換性レイヤーを活用しながら、新規コードから ES Modules を採用していく戦略が有効でしょう。

tsconfig.json の modulemoduleResolution 設定を適切に行い、ESLint で namespace の使用を制限することで、チーム全体で設計指針を統一できます。モジュール設計は一度決めたら終わりではなく、プロジェクトの成長に合わせて継続的に見直していくことが重要です。

関連リンク

著書

とあるクリエイター

フロントエンドエンジニア Next.js / React / TypeScript / Node.js / Docker / AI Coding

;