TypeScriptのnamespaceとmoduleを設計で使い分ける 正しい使い方と判断指針
TypeScript プロジェクトで「namespace と ES Modules、どちらを使うべきか」という判断に迷った経験はないでしょうか。実務で namespace と ES Modules を併用した結果、ビルド時の Tree Shaking が効かずバンドルサイズが肥大化したり、型定義ファイルとの整合性が崩れたりする問題に直面しました。本記事では、併用の落とし穴を整理し、tsconfig.json の設定を含めた現代のモジュール運用で破綻しない選び方を、型安全な設計の観点から解説します。
namespace と ES Modules の比較
| 観点 | namespace | ES 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 Modules | Tree 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 の比較まとめ
| 観点 | namespace | ES Modules |
|---|---|---|
| 推奨用途 | 型定義ファイル(.d.ts)外部 API の型定義レガシー互換レイヤー | アプリケーション開発ライブラリ開発新規プロジェクト全般 |
| Tree Shaking | ❌ 非対応 | ✅ 完全対応 |
| 静的型付け | 宣言マージで拡張可能 | import による型追跡 |
| バンドラー対応 | 限定的 | Vite・webpack 完全対応 |
| tsconfig.json | 特別な設定不要 | module・moduleResolution 設定必須 |
| IDE 支援 | 自動インポートが不安定 | 自動インポート完全対応 |
| テスタビリティ | モック化が困難 | 依存性注入が容易 |
| 将来性 | メンテナンスモード | ECMAScript 標準として発展中 |
向いているケース
namespace が向いているケース:
- サードパーティライブラリの型定義(DefinitelyTyped)
- グローバルに参照される型の論理的グループ化
- 既存の namespace ベースコードとの互換性維持
ES Modules が向いているケース:
- 新規のアプリケーション開発
- バンドルサイズの最適化が重要なプロジェクト
- チーム開発で一貫性を重視する場合
まとめ
TypeScript の namespace と ES Modules は、それぞれ異なる時代の要求に応えて生まれた機能です。現代の開発では ES Modules First の原則を採用し、namespace は型定義など限定的な用途に絞ることで、型安全で保守性の高いコードベースを実現できます。
ただし、レガシーコードを抱えるプロジェクトでは段階的な移行が現実的です。後方互換性レイヤーを活用しながら、新規コードから ES Modules を採用していく戦略が有効でしょう。
tsconfig.json の module と moduleResolution 設定を適切に行い、ESLint で namespace の使用を制限することで、チーム全体で設計指針を統一できます。モジュール設計は一度決めたら終わりではなく、プロジェクトの成長に合わせて継続的に見直していくことが重要です。
関連リンク
著書
article2026年1月23日TypeScriptのtypeとinterfaceを比較・検証する 違いと使い分けの判断基準を整理
article2026年1月23日TypeScript 5.8の型推論を比較・検証する 強化点と落とし穴の回避策
article2026年1月23日TypeScript Genericsの使用例を早見表でまとめる 記法と頻出パターンを整理
article2026年1月22日TypeScriptの型システムを概要で理解する 基礎から全体像まで完全解説
article2026年1月22日ZustandとTypeScriptのユースケース ストアを型安全に設計して運用する実践
article2026年1月22日TypeScriptでよく出るエラーをトラブルシュートでまとめる 原因と解決法30選
articleshadcn/ui × TanStack Table 設計術:仮想化・列リサイズ・アクセシブルなグリッド
articleRemix のデータ境界設計:Loader・Action とクライアントコードの責務分離
articlePreact コンポーネント設計 7 原則:再レンダリング最小化の分割と型付け
articlePHP 8.3 の新機能まとめ:readonly クラス・型強化・性能改善を一気に理解
articleNotebookLM に PDF/Google ドキュメント/URL を取り込む手順と最適化
articlePlaywright 並列実行設計:shard/grep/fixtures で高速化するテストスイート設計術
blogiPhone 17シリーズの発表!全モデルiPhone 16から進化したポイントを見やすく整理
blogGoogleストアから訂正案内!Pixel 10ポイント有効期限「1年」表示は誤りだった
blog【2025年8月】Googleストア「ストアポイント」は1年表記はミス?2年ルールとの整合性を検証
blogGoogleストアの注文キャンセルはなぜ起きる?Pixel 10購入前に知るべき注意点
blogPixcel 10シリーズの発表!全モデル Pixcel 9 から進化したポイントを見やすく整理
blogフロントエンドエンジニアの成長戦略:コーチングで最速スキルアップする方法
review今の自分に満足していますか?『持たざる者の逆襲 まだ何者でもない君へ』溝口勇児
reviewついに語られた業界の裏側!『フジテレビの正体』堀江貴文が描くテレビ局の本当の姿
review愛する勇気を持てば人生が変わる!『幸せになる勇気』岸見一郎・古賀史健のアドラー実践編で真の幸福を手に入れる
review週末を変えれば年収も変わる!『世界の一流は「休日」に何をしているのか』越川慎司の一流週末メソッド
review新しい自分に会いに行こう!『自分の変え方』村岡大樹の認知科学コーチングで人生リセット
review科学革命から AI 時代へ!『サピエンス全史 下巻』ユヴァル・ノア・ハラリが予見する人類の未来
