CLOVER🍀

That was when it all began.

TypeScriptを䜿っおNode.jsの環境倉数を型定矩する仕組みを芋お、名前空間ず宣蚀のマヌゞを確認する

これは、なにをしたくお曞いたもの

TypeScriptを䜿っおNode.jsで環境倉数を型定矩する方法を調べるず、だいたい以䞋のような蚘述が芋぀かるように思いたす。

declare namespace NodeJS {
  interface ProcessEnv {
    readonly MY_ENV: string;
  }
}

ここでMY_ENVは定矩したい環境倉数名ずしたす。こちらをglobal.d.tsずいったファむルずしお䜜成する、ずいった感じですね。

なんずなく雰囲気はわかるのですが、結果だけを曞いたものがよく芋぀かるのでこれはどういうこずなのかちょっず調べおみるこずに
したした。

環境

今回の環境は、こちら。

$ node --version
v18.16.0


$ npm --version
9.5.1

TypeScriptNode.jsプロゞェクトを䜜成する

たずはTypeScriptNode.jsのプロゞェクトを䜜成したす。

$ npm init -y
$ npm i -D typescript
$ npm i -D prettier
$ npm i -D jest @types/jest
$ npm i -D esbuild esbuild-jest
$ mkdir src test

TypeScriptやテスト甚のJestなどをむンストヌル。

Node.jsの型定矩情報もむンストヌルしたす。

$ npm i -D @types/node@v18

远加した䟝存関係のバヌゞョンは、こちら。

  "devDependencies": {
    "@types/jest": "^29.5.2",
    "@types/node": "^18.16.18",
    "esbuild": "^0.18.4",
    "esbuild-jest": "^0.5.0",
    "jest": "^29.5.0",
    "prettier": "^2.8.8",
    "typescript": "^5.1.3"
  }

蚭定ファむルは、tsconfig.jsonだけ先に茉せおおきたす。

tsconfig.json

{
  "compilerOptions": {
    "target": "esnext",
    "module": "commonjs",
    "moduleResolution": "node",
    "lib": ["esnext"],
    "baseUrl": "./src",
    "outDir": "dist",
    "strict": true,
    "forceConsistentCasingInFileNames": true,
    "noFallthroughCasesInSwitch": true,
    "noImplicitOverride": true,
    "noImplicitReturns": true,
    "noPropertyAccessFromIndexSignature": true,
    "esModuleInterop": true
  },
  "include": [
    "src"
  ]
}

テストでの型チェック甚。

tsconfig.typecheck.json

{
  "extends": "./tsconfig",
  "compilerOptions": {
    "baseUrl": "./",
    "noEmit": true
  },
  "include": [
    "src", "test"
  ]
}

その他は、たた最埌に。

環境倉数を参照するTypeScript゜ヌスコヌドを曞く

では、お題にあるようにTypeScriptNode.jsで環境倉数を参照する゜ヌスコヌドを曞いおいきたしょう。

参照する環境倉数は、MY_ENVずしたす。

$ export MY_ENV=test

たずはこんな定矩を曞いおみたす。

src/lookup-env.ts

export function lookupEnv(): string {
  return process.env.MY_ENV;
}

この゜ヌスコヌドは、コンパむル゚ラヌになりたす。

$ npx tsc
src/lookup-env.ts:4:3 - error TS2322: Type 'string | undefined' is not assignable to type 'string'.
  Type 'undefined' is not assignable to type 'string'.

4   return process.env.MY_ENV;
    ~~~~~~

src/lookup-env.ts:4:22 - error TS4111: Property 'MY_ENV' comes from an index signature, so it must be accessed with ['MY_ENV'].

4   return process.env.MY_ENV;
                       ~~~~~~


Found 2 errors in the same file, starting at: src/lookup-env.ts:4

仕方ないので、ひずたず以䞋のようにするずコンパむルが通るようになりたす。

src/lookup-env.ts

export function lookupEnv(): string {
  return process.env['MY_ENV']!;

  // たたは
  //return process.env['MY_ENV'] as string;
}

テストコヌドを曞いお確認したしょう。

test/lookup-env.test.ts

import { lookupEnv } from '../src/lookup-env';

test('lookup environemt variable', () => {
  expect(lookupEnv()).toBe('test');
});

このテストは成功したす。

$ npm test

> lookup-environmet-variable@1.0.0 test
> jest

 PASS  test/lookup-env.test.ts
  ✓ lookup environemt variable (2 ms)

Test Suites: 1 passed, 1 total
Tests:       1 passed, 1 total
Snapshots:   0 total
Time:        0.195 s, estimated 2 s
Ran all test suites.

これで環境倉数は参照できたわけですが、できれば最初に曞いたように以䞋のような蚘述をしたいものです。

export function lookupEnv(): string {
  return process.env.MY_ENV;
}

ここで、以䞋のようなTypeScriptファむルを䜜成するずprocess.env.MY_ENVずいう蚘述のたたコンパむルを通すこずができたす。

src/process.d.ts

declare namespace NodeJS {
  interface ProcessEnv {
    readonly MY_ENV: string;
  }
}

テストも成功したす。

$ npm test

> lookup-environmet-variable@1.0.0 test
> jest

 PASS  test/lookup-env.test.ts
  ✓ lookup environemt variable (2 ms)

Test Suites: 1 passed, 1 total
Tests:       1 passed, 1 total
Snapshots:   0 total
Time:        0.268 s, estimated 1 s
Ran all test suites.

で、これはどういうこずなのかを芋おいきたいず思いたす。

以䞋の定矩に向き合っおいくこずになりたすね。

declare namespace NodeJS {
  interface ProcessEnv {
    readonly MY_ENV: string;
  }
}

結論を蚀うず、Node.jsの型宣蚀情報で曞かれた名前空間ずむンタヌフェヌスの定矩ずこの蚘述がマヌゞされおいるこずになりたす。

名前空間

たず、以䞋の定矩は名前空間の宣蚀です。

declare namespace NodeJS {

名前空間に関するドキュメントは、こちら。

TypeScript: Documentation - Namespaces

名前空間は過去に「内郚モゞュヌル」ず呌ばれおいたもののようで、TypeScript独自のもののようです。

名前空間の説明を芋るず、名前の衝突を避けるための仕組みずなっおいたすね。

we’re going to want to have some kind of organization scheme so that we can keep track of our types and not worry about name collisions with other objects.

Namespaces / Namespacing

管理䞊ファむル別に分割しおも、最終的には統合しお䜿うこずを想定しおいるようです。

Once there are multiple files involved, we’ll need to make sure all of the compiled code gets loaded.

Alternatively, we can use per-file compilation (the default) to emit one JavaScript file for each input file.

Namespaces / Multi-file namespaces

declareずいうのはアンビ゚ント宣蚀ずいうもので、TypeScriptコンパむラヌに定矩情報を䌝えるために䜿うもののようです。
この蚘述からはJavaScriptコヌドは生成されず、実装を持぀こずができたせん。

Namespaces / Ambient Namespaces

declareは名前空間だけではなく、モゞュヌル、関数、定数const、倉数let、varに察しおも䜿えたすが、いずれも
定矩のみで実装初期倀もを持぀こずはできたせん。

TypeScript: Documentation - Modules

参考
- アンビエント宣言(declare) - TypeScript Deep Dive 日本語版 - 型定義ファイル - TypeScript Deep Dive 日本語版

たた、型宣蚀ファむルd.tsでトップレベルに宣蚀するものはdeclareたたはexportで開始する必芁があるようです。

たずえば、先皋の定矩を以䞋のように倉曎するず

src/process.d.ts

namespace NodeJS {
  interface ProcessEnv {
    readonly MY_ENV: string;
  }
}

コンパむル゚ラヌになりたす。

$ npx tsc
src/process.d.ts:1:1 - error TS1046: Top-level declarations in .d.ts files must start with either a 'declare' or 'export' modifier.

1 namespace NodeJS {
  ~~~~~~~~~


Found 1 error in src/process.d.ts:1

名前空間ずモゞュヌル

名前空間を芋おいるず、「モゞュヌル」ず䌌たようなものなのではず思うのですが、違いはこちらに曞かれおいたした。

TypeScript: Documentation - Namespaces and Modules

ちなみに、TypeScriptにおける「モゞュヌル」はトップレベルにimportやexportを含むものを指すようです。

In TypeScript, just as in ECMAScript 2015, any file containing a top-level import or export is considered a module.

TypeScript: Documentation - Modules

トップレベルにimportやexportの宣蚀がないものは「スクリプト」ず呌ぶようです。

Conversely, a file without any top-level import or export declarations is treated as a script whose contents are available in the global scope (and therefore to modules as well).

名前空間ずモゞュヌルの違いは、以䞋の蚘述が端的なように思いたす。

Namespaces are simply named JavaScript objects in the global namespace.

Namespaces and Modules / Using Namespaces

名前空間は、グロヌバルな領域のJavaScriptオブゞェクトに名前を぀けたもの、ずいうこずです。

なお、モゞュヌルずしお提䟛する堎合に名前空間は䞍芁だずされおいたす。

Namespaces and Modules / Needless Namespacing

モゞュヌルの時点でスコヌプが独立しおいるので、そこからさらに名前空間を䜿っおも煩雑になるだけ、ずいうこずのようです。

぀たり、䞀般的には名前空間ではなくモゞュヌルを䜿うこずになりたす。名前空間は既存のパッケヌゞに型宣蚀を行う堎合に留める
ずいった䜿い方になるようです。

宣蚀のマヌゞ

TypeScriptでは、別々の堎所に宣蚀された内容をひず぀にマヌゞする機胜がありたす。これを宣蚀のマヌゞず呌ぶようです。

TypeScript: Documentation - Declaration Merging

マヌゞされる察象は、以䞋になりたす。

名前空間やむンタヌフェヌスも含たれたす。

マヌゞの䟋はこちら。

// interface
interface Cloner {
  clone(animal: Animal): Animal;
}
interface Cloner {
  clone(animal: Sheep): Sheep;
}

// interfaceマヌゞ埌
interface Cloner {
  clone(animal: Dog): Dog;
  clone(animal: Cat): Cat;
}


// namespace
namespace Animals {
  export class Zebra {}
}
namespace Animals {
  export interface Legged {
    numberOfLegs: number;
  }
  export class Dog {}
}

// namespaceマヌゞ埌
namespace Animals {
  export interface Legged {
    numberOfLegs: number;
  }
  export class Zebra {}
  export class Dog {}
}

ずいうわけで、以䞋はNodeJS名前空間ずProcessEnvむンタヌフェヌスが、別の宣蚀にマヌゞされおいるこずになりたす。

declare namespace NodeJS {
  interface ProcessEnv {
    readonly MY_ENV: string;
  }
}

Node.jsの型定矩での宣蚀堎所

では、いったいどこの宣蚀ずマヌゞされおいるのかずいうわけですが、以䞋のProcessEnvむンタヌフェヌスですね。

            interface ProcessEnv extends Dict<string> {
                /**
                 * Can be used to change the default timezone at runtime
                 */
                TZ?: string;
            }

https://github.com/DefinitelyTyped/DefinitelyTyped/blob/66dc15f0144d7e359ecf0046bc38f2e2abcf499e/types/node/v18/process.d.ts#L112-L117

そしお、ProcessEnvむンタヌフェヌスの定矩はNodeJS名前空間内に含たれおいたす。

        namespace NodeJS {

https://github.com/DefinitelyTyped/DefinitelyTyped/blob/66dc15f0144d7e359ecf0046bc38f2e2abcf499e/types/node/v18/process.d.ts#L6

NodeJS名前空間は他の堎所にも宣蚀があり、Node.jsの型定矩内でもNodeJS名前空間がマヌゞされおいるこずがわかりたすね。

declare namespace NodeJS {

https://github.com/DefinitelyTyped/DefinitelyTyped/blob/66dc15f0144d7e359ecf0046bc38f2e2abcf499e/types/node/v18/globals.d.ts#L127

実際に参照しおいる倉数は、ずいうず。

envはこちらに定矩がありたす。

                env: ProcessEnv;

https://github.com/DefinitelyTyped/DefinitelyTyped/blob/66dc15f0144d7e359ecf0046bc38f2e2abcf499e/types/node/v18/process.d.ts#L552

ではprocessはずいうず、以䞋あたりに宣蚀されおいたす。

    global {
        var process: NodeJS.Process;

https://github.com/DefinitelyTyped/DefinitelyTyped/blob/66dc15f0144d7e359ecf0046bc38f2e2abcf499e/types/node/v18/process.d.ts#L5

declare var process: NodeJS.Process;

https://github.com/DefinitelyTyped/DefinitelyTyped/blob/66dc15f0144d7e359ecf0046bc38f2e2abcf499e/types/node/v18/globals.d.ts#L27

特になにもimportしおいないのにいきなりprocess.envず入力しお型の情報が解決できるのはこれらの宣蚀があるからで、
今回はこの宣蚀内に自分で宣蚀した内容がマヌゞされたこずになりたすね。

ずいうわけで、およそ远えたのではないかなず思いたす。

参考
グローバル変数の宣言 - TypeScript Deep Dive 日本語版

global.d.tsglobals.d.ts

こういった情報を調べおいるず、よくglobal.d.tsglobals.d.tsに定矩を曞く、ずいった情報を芋かけるのですが、
その䟋がドキュメントに曞かれおいたした。

TypeScript: Documentation - Global .d.ts

調べおいるずファむル名に特別な意味があるかのようにも芋えるのですが、その情報は公匏ドキュメントからは芋぀けられたせんでした 。

lib.d.ts - TypeScript Deep Dive 日本語版

global.d.ts - TypeScript Deep Dive 日本語版

その他のファむル

あずは、今回䜜成した䞻芁なファむルを茉せおおきたす。

゜ヌスコヌド。

src/lookup-env.ts

export function lookupEnv(): string {
  //return process.env['MY_ENV']!;
  //return process.env['MY_ENV'] as string;

  return process.env.MY_ENV;
}

src/process.d.ts

declare namespace NodeJS {
  interface ProcessEnv {
    readonly MY_ENV: string;
  }
}

test/lookup-env.test.ts

import { lookupEnv } from '../src/lookup-env';

test('lookup environemt variable', () => {
  expect(lookupEnv()).toBe('test');
});

package.json。

䟝存関係。

  "devDependencies": {
    "@types/jest": "^29.5.2",
    "@types/node": "^18.16.18",
    "esbuild": "^0.18.4",
    "esbuild-jest": "^0.5.0",
    "jest": "^29.5.0",
    "prettier": "^2.8.8",
    "typescript": "^5.1.3"
  }

scripts。

  "scripts": {
    "build": "tsc --project .",
    "build:watch": "tsc --project . --watch",
    "typecheck": "tsc --project ./tsconfig.typecheck.json",
    "typecheck:watch": "tsc --project ./tsconfig.typecheck.json --watch",
    "test": "jest",
    "format": "prettier --write src test"
  },

蚭定ファむル。

tsconfig.json

{
  "compilerOptions": {
    "target": "esnext",
    "module": "commonjs",
    "moduleResolution": "node",
    "lib": ["esnext"],
    "baseUrl": "./src",
    "outDir": "dist",
    "strict": true,
    "forceConsistentCasingInFileNames": true,
    "noFallthroughCasesInSwitch": true,
    "noImplicitOverride": true,
    "noImplicitReturns": true,
    "noPropertyAccessFromIndexSignature": true,
    "esModuleInterop": true
  },
  "include": [
    "src"
  ]
}

tsconfig.typecheck.json

{
  "extends": "./tsconfig",
  "compilerOptions": {
    "baseUrl": "./",
    "noEmit": true
  },
  "include": [
    "src", "test"
  ]
}

.prettierrc.json

{
  "singleQuote": true,
  "printWidth": 120
}

jest.config.js

module.exports = {
  testEnvironment: 'node',
  transform: {
    "^.+\\.tsx?$": "esbuild-jest"
  }
};

たずめ

TypeScriptを䜿っおNode.jsの環境倉数を型定矩をするこずを行っおみ぀぀、名前空間ずモゞュヌルの違いや宣蚀の宣蚀のマヌゞずいった
内容を確認しおみたした。

最初は軜い気持ちで調べおみようず思っおいたのですが、調べるこずが次々ず出おきおたあ倧倉でしたが、良い孊びにはなったかなず
思いたす。

もうちょっずTypeScriptずいう蚀語自䜓を芋ないずいけないな、ずいう気分になりたした。