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 比較表
| 機能 | Namespaces | ES 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 移行のステップ
- 各 Namespace を個別のモジュールファイルに変換する。
exportを ES モジュールの export に置き換える。- インポートを ES モジュールの構文(
import { ... })に更新する。 - ビルドシステム(Vite, Webpack 等)をモジュール対応に設定する。
tsconfig.jsonの"module"設定を"ESNext"などに更新する。
9.2 移行ツール
- ts-migrate: Facebook が提供する、Namespace からモジュールへの移行を自動化するツール。
- ESLint:
no-namespaceルールを使用して、新規の Namespace 作成を制限。