1. ホーム
  2. TypeScript

【TypeScript】namespace の使い方|型と実装をまとめる名前空間とモジュールとの使い分けを解説

Share

TypeScript の古いコードや型定義ファイルを読んでいると、namespace Utils { ... } という見慣れない構文に出会うことがあります。これは名前空間(namespace)と呼ばれる、関連する型や関数を1つの名前の下にまとめる仕組みです。現在のアプリケーション開発では ES モジュール(import / export)を使うのが基本ですが、.d.ts による型定義ではいまも現役です。この記事では、namespace の基本的な書き方、ネストや分割、declare namespace の使いどころ、そしてモジュールとの違いと使い分けまでを解説します。

namespace とは(名前の衝突を防ぐための入れ物)

namespace は、型・関数・変数・クラスなどを1つの名前の下にまとめるための構文です。モジュールの仕組みが JavaScript に標準搭載される前は、すべてのスクリプトがグローバルスコープを共有していたため、Validator のような一般的な名前がライブラリ間で衝突しやすいという問題がありました。名前空間はその対策として、MyApp.Validator のように階層を付けて区別できるようにするものです。

コンパイル後は、即時実行関数とオブジェクトを使ったごく普通の JavaScript に変換されます。つまり実行時にも MyApp というオブジェクトが実在し、その中にメンバーがぶら下がる形になります。型だけの仕組みではない、という点が特徴です。

基本の書き方(export したものだけ外から見える)

namespace 名前 { } の中にメンバーを書きます。重要なのは、中で export を付けたものだけが外部から参照できるという点です。付けなかったものは名前空間の内部だけで使えるプライベートなメンバーになります。

validation.ts
namespace Validation {
  // export しないので外からは見えない(内部用)
  const mailPattern = /^[^@\s]+@[^@\s]+\.[^@\s]+$/;

  // 型も export できる
  export type Result = { ok: boolean; message?: string };

  export function isEmail(value: string): boolean {
    return mailPattern.test(value);
  }

  export function validate(value: string): Result {
    return isEmail(value)
      ? { ok: true }
      : { ok: false, message: 'メールアドレスの形式が正しくありません' };
  }
}

// 外からは「名前空間名.メンバー名」で参照する
const result: Validation.Result = Validation.validate('test@example.com');
console.log(result.ok); // true

// console.log(Validation.mailPattern); // エラー: export されていない

型(Result)と値(validate)のどちらも同じ名前空間に入れられ、外からは Validation.ResultValidation.validate() と同じ書き方で参照できます。型と実装をひとまとめに配れるのが名前空間の便利なところです。

ネストと別名(エイリアス)

名前空間は入れ子にできます。階層が深くなると App.Utils.String.trim() のように毎回長く書くことになるので、import 別名 = 名前空間のパス という構文で短い別名を付けられます。これは ES モジュールの import とは別物で、名前空間専用の代入的な書き方です。

nested.ts
namespace App {
  export namespace Utils {
    export function trim(value: string): string {
      return value.trim();
    }
  }

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

// 毎回フルパスで書くと長い
console.log(App.Utils.trim('  hello  '));

// import で別名を付けられる(ES モジュールの import とは別の構文)
import Utils = App.Utils;
console.log(Utils.trim('  hello  '));

// 型にも別名を付けられる
type User = App.Models.User;
const user: User = { id: 1, name: 'yamada' };

同じ名前空間を分割して書ける

同じ名前の namespace を複数回宣言すると、TypeScript はそれらを1つにマージします。1つの名前空間を複数ファイルに分けて書けるということで、大きな名前空間を機能ごとのファイルに分割する使い方ができます。ファイルをまたぐ場合は先頭に /// <reference path="..." /> を書いて依存関係を示し、outFile オプションで1ファイルに連結してから読み込むのが従来のやり方でした。

merge.ts
namespace Shape {
  export function area(width: number, height: number): number {
    return width * height;
  }
}

// 同じ名前で再度宣言すると、前の宣言とマージされる
namespace Shape {
  export function perimeter(width: number, height: number): number {
    return (width + height) * 2;
  }
}

console.log(Shape.area(3, 4)); // 12
console.log(Shape.perimeter(3, 4)); // 14

このマージの性質は、関数やクラスに対しても働きます。関数と同じ名前の名前空間を書くと、その関数に静的なプロパティを型付きで追加できます(宣言のマージ)。ライブラリの型定義で jQuery() が関数でありながら jQuery.ajax も持つ、といった形を表現するのに使われてきた書き方です。

declare namespace で型だけを宣言する

いま namespace にもっとも出番があるのは、.d.ts の型定義ファイルです。declare を付けると実装を書かずに「こういう名前空間が存在する」とだけ宣言でき、CDN で読み込んだライブラリのようにグローバル変数として存在するものに型を与えられます。declare namespace の中では実装が書けないので、export を省略してもすべてのメンバーが公開されます。

global.d.ts
// script タグで読み込んだライブラリ(グローバル変数)に型を付ける
declare namespace MyLib {
  interface Options {
    debug?: boolean;
    timeout?: number;
  }

  function init(options?: Options): void;
  const version: string;
}

// 別ファイルの .ts から型付きで使える
MyLib.init({ debug: true });
console.log(MyLib.version);

なお、この形が成立するのは .d.ts がグローバル宣言ファイルとして扱われている場合です。ファイル内にトップレベルの import / export があると、そのファイル自体がモジュールとみなされ、宣言はグローバルには公開されません。その場合は declare global { } で囲む必要があります。

モジュールとの使い分け

アプリケーションのコードでは、名前空間ではなく ES モジュールを使うのが現在の推奨です。TypeScript の公式ドキュメントでも、モジュールが使える環境で名前空間を新規に導入することは勧められていません。バンドラーによる未使用コードの削除(tree shaking)が効きやすい、ファイルと公開範囲の対応が分かりやすい、といった利点があるためです。

観点namespaceES モジュール
読み込み方グローバルに配置され、そのまま参照import で明示的に読み込む
ファイル分割同名宣言でマージ、reference で依存指定ファイル単位で自然に分割
未使用コードの削除効きにくいバンドラーが削除しやすい
主な使いどころグローバル変数の型定義(.d.tsアプリケーションのコード全般

モジュールで同じことをしたいなら、ファイルそのものが名前空間の役割を果たします。validation.ts から必要なものを export し、使う側で import * as Validation from './validation' と書けば、Validation.validate() という同じ呼び出し方を保ったままモジュール化できます。

思ったように参照できないとき

ファイルに import / export があると名前空間が届かない

namespace を書いたファイルにトップレベルの importexport が1つでもあると、そのファイルはモジュールになります。モジュール内の名前空間はグローバルには公開されないため、別ファイルから Validation.validate() と書いても「名前 ‘Validation’ が見つかりません」となります。この場合は名前空間ごと export して、使う側で import { Validation } from './validation' するか、素直にモジュールの export に置き換えてください。

export を付け忘れている

名前空間の中のメンバーは、既定では外に公開されません。「関数はあるのに Validation.isEmail が存在しない」と言われるときは、その関数に export が付いているかを確認します。逆に declare namespace の中では export なしでも公開されるため、通常の namespace と挙動が違う点に注意してください。

ESLint に「名前空間は使うな」と怒られる

TypeScript ESLint の no-namespace ルールは、既定で namespace の使用を警告します。.d.ts での宣言は既定で許可されていますが、.ts で必要な事情がある場合はルールのオプション(allowDeclarations など)で調整するか、モジュールへの書き換えを検討しましょう。

まとめ

namespace は、型や関数を1つの名前の下にまとめてグローバルでの名前の衝突を避けるための、TypeScript 独自の構文です。中で export を付けたメンバーだけが外部から参照でき、入れ子や import 別名 = ... による短縮、同名宣言のマージによるファイル分割ができます。もっとも実用的な使いどころは .d.ts での declare namespace で、script タグで読み込むライブラリのようなグローバル変数に型を与えられます。一方、アプリケーションのコードでは ES モジュールを使うのが現在の標準です。ファイル自体が名前空間の役割を果たし、import * as Validation from './validation' と書けば同じ呼び出し方も再現できます。既存コードや型定義を読むために namespace の仕組みを理解しつつ、新しく書くコードではモジュールを選ぶ、という使い分けがおすすめです。

参考ページ