✍️

[TS TIP] アンビエント宣言とネームスペース

に公開

最初に

普段あまり意識することのない「アンビエント宣言」と「namespace」について触れる機会があったので、少し調べた内容を噛み砕いて書き留めておこうと思います。

実際知らなくても困ることはないですが、知っていて損はないなと感じたので、
気になる方は読み進めていただければと思います。

では始めます。

アンビエント宣言(declare)とは?

一言で表すと「この変数や関数、型などは外部にすでに存在しているよ!」という宣言のことです。

Javascriptのコードには存在しない(型が)けど、「型情報としては必要」なものを Typescript に伝えるための構文です。

例えば、外部ライブラリに関数は存在しているが、Typescirptで使用する際は型安全な状態で使用したいといった場合に使用できます。

外部に関数がある。

someLib.js
function doSomething(val) {
  return val * 2;
}

Typescirptに対してこの関数はこの型ですでに存在しているよと宣言(declare)する。

global.d.ts
declare function doSomething(val: number): number;

実際に使用する。

main.ts
const result = doSomething(5);

といった具合です。

ここで重要なのは、declareはコンパイル対象外となるので、Javaspriptには出力されないと言うことです。

declareはあくまで宣言であるため定義とは別物と考えるべきであり、
すでに存在する関数の処理をわざわざ上書きする必要がなく、Typescriptで扱いやすくするためにあるということになります。

namespace(名前空間)とは?

一言で表すと「型や定数、関数などをグループにまとめて名前の衝突を防ぐ機能」のことです。

Javascriptではグローバルに定義しすぎると名前被りが原因でエラーが発生することがあります。
Typescriptのnamespaceはそれを防ぐための仕組みです。

例えば、同じ名前だがそれぞれ処理が異なる関数を一つのファイルで呼び出したい時を考えます。

「名前空間の衝突の場合」

fileA.js
function showMessage() {
  console.log("Hello from A");
}
fileB.js
function showMessage() {
  console.log("Hello from B");
}

呼び出す!(けど同じ関数名だからもちろんエラー)

index.html
<script src="fileA.js"></script>
<script src="fileB.js"></script>

こうなれば、fileB.jsのshowMessage()がfileA.jsの関数を上書きしてしまいます。

「Typescriptのnamespaceで回避する」

fileA.ts
namespace ModuleA {
  export function showMessage() {
    console.log("Hello from A");
  }
}
fileB.ts
namespace ModuleB {
  export function showMessage() {
    console.log("Hello from B");
  }
}
main.ts
import { ModuleA } form "./fileA"
import { ModuleB } form "./fileB"

ModuleA.showMessage(); // Hello from A
ModuleB.showMessage(); // Hello from B

こうすれば、名前空間を通してexportされた関数を呼び出せるため、同じ箇所で同じ関数を宣言してもエラーにならず処理できます。

まぁ、わざわざこうしなくても、ES Modules(import/exportを使うモダン構成)では、代わりにモジュール構文で以下のように自然に名前衝突を回避できるのでnamespaceを使用するかどうかは考え方次第かと思いますが、命名にばらつきが出てしまう可能性も考慮するとnamespaceで設定した値をimportしてくる方が全体的に統一性が増すのではとも思います。

fileA.ts
export function showMessage() {
  console.log("Hello from A");
}
fileB.ts
export function showMessage() {
  console.log("Hello from B");
}
main.ts
import * as ModuleA from './fileA';
import * as ModuleB from './fileB';

ModuleA.showMessage(); // Hello from A
ModuleB.showMessage(); // Hello from B

組み合わせてみよう

前述したようにグローバル変数に対して使用するケースを考えてみましょう。

type.d.ts
declare namespace NodeJS {
  interface ProcessEnv {
    DATABASE_URL: string;
    STAGE: 'dev' | 'stg' | 'prd';
  }
}

declareはTypescirptにこの型が存在しているよと教えています。
また、グローバル変数(PorcessEnv)に対してこちらで用意したキーがこの型であることを教えてあげるためには、すでに名前空間で定義されているNodeJSの中にあるProcessEnvに対して宣言をしてあげる必要があります。

こうすることで、ProcessEnvがデフォルトでは string | undefinedとなるところが、厳密な型定義のおかげで決められた型で返すようになるので非nullアサーション演算子(!)を使用することなく定義できるようになります。

まとめ

普段意識しなくても別に良いと思っていた機能ですが、ライブラリのコードをあさってみるとたまに見かけることがあり、理解すると選択肢の一つとして有益な機能だなと感じました。

コードの一貫性の担保や実装を進めやすくするためにもこれらの機能は活用していきたいと思います。

今回の記事が誰かのお役に立てれば幸いです。

NCDCエンジニアブログ

Discussion